76 Commits

Author SHA1 Message Date
Simone Margaritelli
1615fc8817 releasing v1.4.1 2019-12-07 15:45:15 +02:00
Simone Margaritelli
714cb00610 misc: small fix or general refactoring i did not bother commenting 2019-12-07 15:44:03 +02:00
Simone Margaritelli
a0a790635a fix: fuck me for trusting people's PR without checking 10000 times 2019-12-07 15:43:10 +02:00
Simone Margaritelli
9d17be959d releasing v1.4.0 2019-12-07 15:08:17 +02:00
evilsocket
4b651cd17b Merge pull request #694 from Evg33/fr_waveshare144lcd
waveshare 1.44inch lcd hat
2019-12-07 14:11:25 +02:00
evilsocket
2f70512076 Merge pull request #693 from Nels885/improve-plugins-web-page
improvement of the plugins web page
2019-12-07 14:10:37 +02:00
evilsocket
b79c59c639 Merge pull request #698 from dadav/feature/usr1_handler
Add signal handler
2019-12-07 14:09:36 +02:00
evilsocket
56f7b67699 Merge pull request #699 from dadav/fix/add-lock-to-ohc
Add lock to onlinehashcrack plugin
2019-12-07 14:09:05 +02:00
dadav
1a8472268e Add signal handler 2019-12-07 09:36:56 +01:00
dadav
4fb7205281 Add lock 2019-12-07 09:30:05 +01:00
Evg33
b4daf19401 layout redesign 2019-12-06 20:11:37 +03:00
evilsocket
e04e053cee Merge pull request #690 from alanyee/patch-1
Replace string formatting with logging laziness in agent.py
2019-12-06 15:04:45 +02:00
evilsocket
4f153e3899 Merge pull request #689 from dadav/fix/bt-tether-lock
Add lock to bt-tether
2019-12-06 15:01:29 +02:00
Evg33
04720ecc42 memtemp plugin for waveshare.com/1.44inch-lcd-hat 2019-12-06 03:24:44 +03:00
Evg33
a12e2aafa5 gps plugin for waveshare.com/1.44inch-lcd-hat 2019-12-06 03:24:24 +03:00
Evg33
1721f67ec3 fr waveshare.com/1.44inch-lcd-hat 2019-12-06 03:24:01 +03:00
Nels885
7693e42aa1 Change the name of the CSS class 'element' to 'plugins-box' and centering the text 2019-12-05 11:49:34 +01:00
Nels885
2ae48a2ef2 Changing the display style of the plugins page 2019-12-05 11:15:29 +01:00
Alan Yee
663bca41cd Update agent.py
Leverage logging laziness rather than string formatting
2019-12-03 11:34:40 -08:00
dadav
ede01e50cd Add lock 2019-12-03 18:15:35 +01:00
evilsocket
19973574e7 Merge pull request #677 from dadav/fix/plugins_setup_ui
Fix/plugins setup ui
2019-12-03 11:44:57 +01:00
evilsocket
a019b4c778 Merge pull request #678 from dadav/fix/net-pos-bug
Add lock; make less verbose
2019-12-03 11:44:12 +01:00
evilsocket
526c5bed87 Merge pull request #680 from Nels885/update-french-translation
Update french translation
2019-12-03 11:42:47 +01:00
evilsocket
4d6136633a Merge pull request #682 from jakubmilkowski/master
fix: Prevent duplicate entries for uploaded pcaps
2019-12-03 11:42:22 +01:00
evilsocket
6d90b75d10 Merge pull request #684 from dadav/feature/session_stats_plugin
Add session-stats plugin
2019-12-03 11:41:20 +01:00
evilsocket
03695be807 Merge pull request #687 from alanyee/patch-1
Use list comprehension in setup.py
2019-12-03 11:40:34 +01:00
Alan Yee
6a97476732 Update setup.py
Use list comprehension
2019-12-02 13:32:36 -08:00
dadav
b5e620684b this is apparently needed 2019-12-02 19:22:36 +01:00
dadav
95557ab37d Axes information will be lost otherwise 2019-12-02 19:20:47 +01:00
dadav
988d093e36 Add session-stats plugin 2019-12-01 21:49:27 +01:00
jakubmilkowski
6e57e131b3 fix: Prevent duplicate entries for uploaded pcaps
For the same reasons like described here https://github.com/evilsocket/pwnagotchi/issues/657 duplicated entries could be created in /root/.ohc_uploads

Signed-off-by: jakubmilkowski <jakub.milkowsky@gmail.com>
2019-11-30 22:44:03 +01:00
dadav
548b42ef20 Lock ui on change 2019-11-30 14:20:37 +01:00
dadav
99614c8cd4 Call on_ready 2019-11-30 14:01:35 +01:00
VOIRIN Lionel
e19ea999e2 Correction of some French translations 2019-11-30 12:51:53 +01:00
VOIRIN Lionel
2207a1eacf Updating the French translation 2019-11-30 10:54:38 +01:00
dadav
9509dd0aa5 Add lock; make less verbose 2019-11-30 10:05:43 +01:00
dadav
608904daf8 Call unload with ui arg 2019-11-30 09:43:39 +01:00
dadav
f973997cdb Call on_ui_setup when plugin reloads 2019-11-30 09:35:19 +01:00
evilsocket
9b594f7fb2 Merge pull request #669 from cdiemel/master
added on_unfiltered_wifi_list
2019-11-29 15:45:43 +01:00
evilsocket
b8eed4f52a Merge pull request #671 from Evg33/reboot
feature/plugin/web/reboot
2019-11-29 15:45:26 +01:00
evilsocket
ad510429fe Merge pull request #672 from dadav/feature/plugins_urls
Add urls to plugins
2019-11-29 15:44:58 +01:00
dadav
f5a94fde96 Add url to plugin 2019-11-28 21:55:20 +01:00
Evg33
855bda9104 feature/plugin/web/reboot 2019-11-28 23:02:22 +03:00
Casey Diemel
e72fd08fb4 added on_unfiltered_wifi_list
Signed-off-by: Casey Diemel <diemelcw@gmail.com>
2019-11-28 14:39:15 -05:00
evilsocket
e44ebac43f Merge pull request #627 from soebbing/improve-german-translations
Improve German translations slightly
2019-11-28 11:29:30 +01:00
evilsocket
b5ddb716e2 Merge pull request #660 from mbgroot/master
Updating the Russian translation
2019-11-28 11:28:54 +01:00
evilsocket
82e7e09fa1 Merge pull request #664 from dadav/feature/wpa-sec-download
Add wpa-sec password download
2019-11-28 11:28:36 +01:00
evilsocket
8b40e94ca8 Merge pull request #665 from dadav/feature/plugins_web_page
Add plugins page
2019-11-28 11:28:15 +01:00
dadav
cc5c46906f Add plugins page 2019-11-27 21:22:40 +01:00
dadav
7cb52ba33a Add wpa-sec password download 2019-11-27 18:51:37 +01:00
Evgeny Zelenin
07f8e7bd4a Update voice.po 2019-11-27 22:01:43 +05:00
Evgeny Zelenin
48dc751d13 Update voice.po 2019-11-27 12:09:24 +05:00
Evgeny Zelenin
3c154ffe0c Update voice.po 2019-11-27 02:58:16 +05:00
Evgeny Zelenin
167f559d73 Update voice.po 2019-11-27 02:54:48 +05:00
evilsocket
19775b7d27 Merge pull request #654 from dadav/fix/webcfg_check_for_error
Fix/webcfg check for error
2019-11-26 12:32:10 +01:00
evilsocket
48e3a372cc Merge pull request #658 from sayak-brm/master
[BUGFIX] Prevent duplicate entries for reported networks
2019-11-26 12:04:00 +01:00
Sayak Brahmachari
d2c44797e5 Prevent duplicate entries for reported networks
Due to duplicate entries in `/root/.api-report.json`, [this code](https://github.com/evilsocket/pwnagotchi/blob/master/pwnagotchi/plugins/default/grid.py#L90) incorrectly reports the number of pwned networks, resulting in incorrect stats on the [pwnagotchi.ai website](https://pwnagotchi.ai/).
2019-11-26 14:05:10 +05:30
dadav
a03443986b Parse to str 2019-11-25 20:08:20 +01:00
dadav
a7ea499fac Should fail before write 2019-11-25 19:47:23 +01:00
evilsocket
722a91655a Merge pull request #649 from xenDE/patch-4
webgpsmap: add filter for: SSID, MAC, isCracked, Password
2019-11-25 12:03:49 +01:00
xenDE
93e06d7f59 add filter for: SSID, MAC, isCracked, Password 2019-11-24 18:49:32 +01:00
evilsocket
7de5121033 Merge pull request #645 from xenDE/patch-3
fix gpio_buttons plugin: gpio needs to be a number
2019-11-23 11:30:28 +01:00
xenDE
83f741bbb0 fix: gpio needs to be a number
fixes gpio id as string in config
https://github.com/evilsocket/pwnagotchi/issues/643
2019-11-23 02:05:01 +01:00
evilsocket
a779fb9b0b Merge pull request #640 from xenDE/patch-2
cleanup, fixes and add handling of .paw-gps.json
2019-11-21 17:03:08 +01:00
xenDE
c4a007e72a cleanup, fixes and add handling of .paw-gps.json
cleanup logging, increase cache item count from 1024 to 2048, add handling of corrupt long/lat position null, added handling of .paw-gps.json files - tested with master
2019-11-20 15:45:16 +01:00
evilsocket
1a71615fa8 Merge pull request #635 from Arttumiro/patch-3
Update to using .paw-gps.json files
2019-11-20 10:11:16 +01:00
evilsocket
7a9f84f495 Merge pull request #638 from xslendix/master
Added romanian language
2019-11-20 10:11:06 +01:00
evilsocket
6e3f5a1181 Merge pull request #632 from daniel156161/master
fix backup.sh (find with type f for no zero byte files into archive)
2019-11-20 10:10:33 +01:00
root
d045ed5afa Added romanian language 2019-11-19 22:58:49 +00:00
Arttumiro
0ee0aaff37 Update to using .paw-gps.json files
Remove old link due to it not being too good, update file extension to .paw-gps.json for better support on problems with webgpsmap.
2019-11-19 15:38:11 +02:00
evilsocket
0fb81a11c4 Merge pull request #629 from xenDE/patch-1
fix gps iso-datetime parsing
2019-11-19 11:30:15 +01:00
daniel156161
cfc0ad1b48 fix backup.sh (find with type f for no zero byte files into archive) 2019-11-19 05:03:21 +01:00
xenDE
3351c251ef fix gps timestamp parsing
problem on timestamp parsing if microseconds are more then 6 numbers.

will fix bug reported in this pr: https://github.com/evilsocket/pwnagotchi/pull/619

tested with testdata from https://github.com/xenDE/pwnagotchi-plugin-webgpsmap/tree/master/handshakes.gps-map-test

before:

[2019-11-19 00:37:51,946] [INFO] webgpsmap: scanning /root/handshakes.gps-map-test
[2019-11-19 00:37:52,022] [INFO] webgpsmap: Found 4 .(geo|gps).json files from 5 handshakes. Fetching positions ...
[2019-11-19 00:37:52,144] [ERROR] Lng is 0
[2019-11-19 00:37:52,241] [ERROR] Invalid isoformat string: '2019-11-14T12:30:41.097414739+01:00'
[2019-11-19 00:37:52,280] [ERROR] Lng is 0
[2019-11-19 00:37:52,329] [INFO] webgpsmap loaded 2 positions

after:

[2019-11-19 00:48:04,652] [INFO] webgpsmap: scanning /root/handshakes.gps-map-test
[2019-11-19 00:48:04,693] [INFO] webgpsmap: Found 5 .(geo|gps).json files from 6 handshakes. Fetching positions ...
[2019-11-19 00:48:04,760] [ERROR] Lng is 0
[2019-11-19 00:48:04,822] [ERROR] Lng is 0
[2019-11-19 00:48:04,850] [INFO] webgpsmap loaded 3 positions
2019-11-19 00:56:59 +01:00
Hendrik Söbbing
d9d399429c Improve German translations slightly
Signed-off-by: Hendrik Söbbing <h.soebbing@shopware.com>
2019-11-18 18:31:11 +01:00
evilsocket
b5a148f287 Merge pull request #625 from neutralinsomniac/invert_led_blink
fix: apparently for the led, 0 is ON and 1 is OFF
2019-11-18 15:56:50 +01:00
Jeremy O'Brien
7138f6469b fix: apparently for the led, 0 is ON and 1 is OFF
Signed-off-by: Jeremy O'Brien <neutral@fastmail.com>
2019-11-18 08:57:10 -05:00
78 changed files with 29895 additions and 405 deletions

View File

@@ -3,6 +3,7 @@ import logging
import argparse
import time
import yaml
import signal
import pwnagotchi
import pwnagotchi.grid as grid
@@ -12,6 +13,7 @@ import pwnagotchi.plugins as plugins
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display
from pwnagotchi import restart
def do_clear(display):
@@ -134,6 +136,12 @@ if __name__ == '__main__':
agent = Agent(view=display, config=config, keypair=KeyPair(view=display))
def usr1_handler(*unused):
logging.info('Received USR1 singal. Restart process ...')
restart("MANU" if args.do_manual else "AUTO")
signal.signal(signal.SIGUSR1, usr1_handler)
if args.do_manual:
do_manual_mode(agent)
else:

View File

@@ -6,7 +6,7 @@ import re
import pwnagotchi.ui.view as view
import pwnagotchi
version = '1.3.0'
version = '1.4.1'
_name = None

View File

@@ -49,9 +49,9 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if not os.path.exists(config['bettercap']['handshakes']):
os.makedirs(config['bettercap']['handshakes'])
logging.info("%s@%s (v%s)" % (pwnagotchi.name(), self.fingerprint(), pwnagotchi.version))
logging.info("%s@%s (v%s)", pwnagotchi.name(), self.fingerprint(), pwnagotchi.version)
for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s" % (plugin.__class__.__name__, plugin.__version__))
logging.debug("plugin '%s' v%s", plugin.__class__.__name__, plugin.__version__)
def config(self):
return self._config
@@ -63,7 +63,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
return self._supported_channels
def setup_events(self):
logging.info("connecting to %s ..." % self.url)
logging.info("connecting to %s ...", self.url)
for tag in self._config['bettercap']['silence']:
try:
@@ -90,7 +90,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
s = self.session()
for iface in s['interfaces']:
if iface['name'] == mon_iface:
logging.info("found monitor interface: %s" % iface['name'])
logging.info("found monitor interface: %s", iface['name'])
has_mon = True
break
@@ -99,11 +99,11 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
logging.info("starting monitor interface ...")
self.run('!%s' % mon_start_cmd)
else:
logging.info("waiting for monitor interface %s ..." % mon_iface)
logging.info("waiting for monitor interface %s ...", mon_iface)
time.sleep(1)
logging.info("supported channels: %s" % self._supported_channels)
logging.info("handshakes will be collected inside %s" % self._config['bettercap']['handshakes'])
logging.info("supported channels: %s", self._supported_channels)
logging.info("handshakes will be collected inside %s", self._config['bettercap']['handshakes'])
self._reset_wifi_settings()
@@ -151,10 +151,10 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if not channels:
self._current_channel = 0
logging.debug("RECON %ds" % recon_time)
logging.debug("RECON %ds", recon_time)
self.run('wifi.recon.channel clear')
else:
logging.debug("RECON %ds ON CHANNELS %s" % (recon_time, ','.join(map(str, channels))))
logging.debug("RECON %ds ON CHANNELS %s", recon_time, ','.join(map(str, channels)))
try:
self.run('wifi.recon.channel %s' % ','.join(map(str, channels)))
except Exception as e:
@@ -212,7 +212,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
ch = ap['channel']
# if we're sticking to a channel, skip anything
# which is not on that channel
if channels != [] and ch not in channels:
if not channels and ch not in channels:
continue
if ch not in grouped:
@@ -274,7 +274,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
pwnagotchi.reboot()
def _save_recovery_data(self):
logging.warning("writing recovery data to %s ..." % RECOVERY_DATA_FILE)
logging.warning("writing recovery data to %s ...", RECOVERY_DATA_FILE)
with open(RECOVERY_DATA_FILE, 'w') as fp:
data = {
'started_at': self._started_at,
@@ -289,7 +289,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
try:
with open(RECOVERY_DATA_FILE, 'rt') as fp:
data = json.load(fp)
logging.info("found recovery data: %s" % data)
logging.info("found recovery data: %s", data)
self._started_at = data['started_at']
self._epoch.epoch = data['epoch']
self._handshakes = data['handshakes']
@@ -297,7 +297,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._last_pwnd = data['last_pwnd']
if delete:
logging.info("deleting %s" % RECOVERY_DATA_FILE)
logging.info("deleting %s", RECOVERY_DATA_FILE)
os.unlink(RECOVERY_DATA_FILE)
except:
if not no_exceptions:
@@ -334,7 +334,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
new_shakes += 1
ap_and_station = self._find_ap_sta_in(sta_mac, ap_mac, s)
if ap_and_station is None:
logging.warning("!!! captured new handshake: %s !!!" % key)
logging.warning("!!! captured new handshake: %s !!!", key)
self._last_pwnd = ap_mac
plugins.on('handshake', self, filename, ap_mac, sta_mac)
else:
@@ -342,15 +342,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
self._last_pwnd = ap['hostname'] if ap['hostname'] != '' and ap[
'hostname'] != '<hidden>' else ap_mac
logging.warning(
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!" % (
"!!! captured new handshake on channel %d, %d dBm: %s (%s) -> %s [%s (%s)] !!!",
ap['channel'],
ap['rssi'],
sta['mac'], sta['vendor'],
ap['hostname'], ap['mac'], ap['vendor']))
ap['hostname'], ap['mac'], ap['vendor'])
plugins.on('handshake', self, filename, ap, sta)
except Exception as e:
logging.error("error: %s" % e)
logging.error("error: %s", e)
finally:
self._update_handshakes(new_shakes)
@@ -392,15 +392,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def associate(self, ap, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping assoc(%s)" % ap['mac'])
logging.debug("recon is stale, skipping assoc(%s)", ap['mac'])
return
if self._config['personality']['associate'] and self._should_interact(ap['mac']):
self._view.on_assoc(ap)
try:
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm..." % ( \
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi']))
logging.info("sending association frame to %s (%s %s) on channel %d [%d clients], %d dBm...",
ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], len(ap['clients']), ap['rssi'])
self.run('wifi.assoc %s' % ap['mac'])
self._epoch.track(assoc=True)
except Exception as e:
@@ -413,15 +413,15 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def deauth(self, ap, sta, throttle=0):
if self.is_stale():
logging.debug("recon is stale, skipping deauth(%s)" % sta['mac'])
logging.debug("recon is stale, skipping deauth(%s)", sta['mac'])
return
if self._config['personality']['deauth'] and self._should_interact(sta['mac']):
self._view.on_deauth(sta)
try:
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ..." % (
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi']))
logging.info("deauthing %s (%s) from %s (%s %s) on channel %d, %d dBm ...",
sta['mac'], sta['vendor'], ap['hostname'], ap['mac'], ap['vendor'], ap['channel'], ap['rssi'])
self.run('wifi.deauth %s' % sta['mac'])
self._epoch.track(deauth=True)
except Exception as e:
@@ -434,7 +434,7 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
def set_channel(self, channel, verbose=True):
if self.is_stale():
logging.debug("recon is stale, skipping set_channel(%d)" % channel)
logging.debug("recon is stale, skipping set_channel(%d)", channel)
return
# if in the previous loop no client stations has been deauthenticated
@@ -450,12 +450,12 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
if channel != self._current_channel:
if self._current_channel != 0 and wait > 0:
if verbose:
logging.info("waiting for %ds on channel %d ..." % (wait, self._current_channel))
logging.info("waiting for %ds on channel %d ...", wait, self._current_channel)
else:
logging.debug("waiting for %ds on channel %d ..." % (wait, self._current_channel))
logging.debug("waiting for %ds on channel %d ...", wait, self._current_channel)
self.wait_for(wait)
if verbose and self._epoch.any_activity:
logging.info("CHANNEL %d" % channel)
logging.info("CHANNEL %d", channel)
try:
self.run('wifi.recon.channel %d' % channel)
self._current_channel = channel
@@ -465,4 +465,4 @@ class Agent(Client, Automata, AsyncAdvertiser, AsyncTrainer):
plugins.on('channel_hop', self, channel)
except Exception as e:
logging.error("error: %s" % e)
logging.error("error: %s", e)

View File

@@ -39,6 +39,7 @@ main:
enabled: false
api_key: ~
api_url: "https://wpa-sec.stanev.org"
download_results: false
wigle:
enabled: false
api_key: ~
@@ -114,6 +115,8 @@ main:
peer_lost: 'oo oo oo oo oo oo oo'
webcfg:
enabled: false
session-stats:
enabled: true
# monitor interface to use
iface: mon0
# command to run to bring the mon interface up in case it's not up already
@@ -257,7 +260,7 @@ ui:
display:
enabled: true
rotation: 180
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, lcdhat, waveshare154inch, waveshare27inch, waveshare29inch, dfrobot/df
# Possible options inkyphat/inky, papirus/papi, waveshare_1/ws_1 or waveshare_2/ws_2, oledhat, lcdhat, waveshare154inch, waveshare27inch, waveshare29inch, dfrobot/df, waveshare144lcd/ws144inch
type: 'waveshare_2'
# Possible options red/yellow/black (black used for monocromatic displays)
# Waveshare tri-color 2.13in display can be over-driven with color set as 'fastAndFurious'

View File

@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-23 20:56+0200\n"
"POT-Creation-Date: 2019-11-14 21:15+0100\n"
"PO-Revision-Date: 2019-09-29 14:00+0200\n"
"Last-Translator: dadav <33197631+dadav@users.noreply.github.com>\n"
"Language-Team: DE <33197631+dadav@users.noreply.github.com>\n"
@@ -20,7 +20,7 @@ msgid "ZzzzZZzzzzZzzz"
msgstr ""
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Hi, ich bin ein Pwnagotchi! Starte ..."
msgstr "Hi, ich bin ein Pwnagotchi! Starte..."
msgid "New day, new hunt, new pwns!"
msgstr "Neuer Tag, neue Jagd, neue Pwns!"
@@ -35,23 +35,30 @@ msgid "The neural network is ready."
msgstr "Das neurale Netz ist bereit."
msgid "Generating keys, do not turn off ..."
msgstr "Generiere Keys, nicht ausschalten ..."
msgstr "Generiere Keys, nicht ausschalten..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, Channel {channel} ist frei! Dein AP wir des dir danken."
msgstr "Hey, Channel {channel} ist frei! Dein AP wird es Dir danken."
msgid "Reading last session logs ..."
msgstr "Lese die Logs der letzten Session..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Bisher {lines_so_far} Zeilen im Log gelesen..."
msgid "I'm bored ..."
msgstr "Mir ist langweilig..."
msgid "Let's go for a walk!"
msgstr "Lass uns laufen gehen!"
msgstr "Lass uns spazieren gehen!"
msgid "This is the best day of my life!"
msgstr "Das ist der beste Tag meines Lebens."
msgstr "Das ist der beste Tag meines Lebens!"
msgid "Shitty day :/"
msgstr "Scheis Tag :/"
msgstr "Scheißtag :/"
msgid "I'm extremely bored ..."
msgstr "Mir ist sau langweilig..."
@@ -62,6 +69,13 @@ msgstr "Ich bin sehr traurig..."
msgid "I'm sad"
msgstr "Ich bin traurig"
#, fuzzy
msgid "Leave me alone ..."
msgstr "Lass mich in Ruhe..."
msgid "I'm mad at you!"
msgstr "Ich bin sauer auf Dich!"
msgid "I'm living the life!"
msgstr "Ich lebe das Leben!"
@@ -75,27 +89,35 @@ msgid "I'm having so much fun!"
msgstr "Ich habe sooo viel Spaß!"
msgid "My crime is that of curiosity ..."
msgstr "Mein Verbrechen ist das der Neugier ..."
msgstr "Mein Verbrechen ist das der Neugier..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Hallo {name}, nett Dich kennenzulernen."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Jo {name}! Was geht!?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {name}, wie geht's?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Gerät {name} ist in der nähe!!"
msgstr "Gerät {name} ist in der Nähe!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm ...tschüß {name}"
msgstr "Uhm... tschüß {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} ist weg ..."
msgstr "{name} ist weg..."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Whoops ...{name} ist weg."
msgstr "Whoops... {name} ist weg."
#, python-brace-format
msgid "{name} missed!"
@@ -111,17 +133,17 @@ msgid "I love my friends!"
msgstr "Ich liebe meine Freunde!"
msgid "Nobody wants to play with me ..."
msgstr "Niemand will mit mir spielen ..."
msgstr "Niemand will mit mir spielen..."
msgid "I feel so alone ..."
msgstr "Ich fühl michso alleine ..."
msgstr "Ich fühl' mich so allein..."
msgid "Where's everybody?!"
msgstr "Wo sind denn alle?"
msgstr "Wo sind denn alle?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Schlafe für {secs}s"
msgstr "Schlafe für {secs}s..."
msgid "Zzzzz"
msgstr ""
@@ -138,7 +160,7 @@ msgstr ""
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Warte für {secs}s ..."
msgstr "Warte für {secs}s..."
#, python-brace-format
msgid "Looking around ({secs}s)"
@@ -158,7 +180,7 @@ msgstr "Jo {what}!"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Ich denke, dass {mac} kein WiFi brauch!"
msgstr "Ich denke, dass {mac} kein WiFi braucht!"
#, python-brace-format
msgid "Deauthenticating {mac}"
@@ -177,7 +199,7 @@ msgid "You have {count} new message{plural}!"
msgstr "Cool, wir haben {num} neue Handshake{plural}!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "Ops, da ist etwas schief gelaufen ...Starte neu ..."
msgstr "Ops, da ist was schief gelaufen... Starte neu..."
#, python-brace-format
msgid "Kicked {num} stations\n"

View File

@@ -3,12 +3,11 @@
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR <7271496+quantumsheep@users.noreply.github.com>, 2019.
#
#,
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-23 18:37+0200\n"
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
"PO-Revision-Date: 2019-10-03 10:34+0200\n"
"Last-Translator: quantumsheep <7271496+quantumsheep@users.noreply.github."
"com>\n"
@@ -43,6 +42,13 @@ msgstr "Génération des clés, ne pas éteindre..."
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, le canal {channel} est libre! Ton point d'accès va te remercier."
msgid "Reading last session logs ..."
msgstr "Lecture des logs de la dernière session ..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Jusqu'ici, {lines_so_far} lignes lues dans le log ..."
msgid "I'm bored ..."
msgstr "Je m'ennuie..."
@@ -64,6 +70,13 @@ msgstr "Je suis très triste..."
msgid "I'm sad"
msgstr "Je suis triste"
#, fuzzy
msgid "Leave me alone ..."
msgstr "Je me sens si seul..."
msgid "I'm mad at you!"
msgstr "Je t'en veux !"
msgid "I'm living the life!"
msgstr "Je vis la vie !"
@@ -83,6 +96,14 @@ msgstr "Mon crime, c'est la curiosité..."
msgid "Hello {name}! Nice to meet you."
msgstr "Bonjour {name} ! Ravi de te rencontrer."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name} ! Quoi de neuf ?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {name} comment vas-tu ?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "L'unité {name} est proche !"
@@ -106,6 +127,12 @@ msgstr "{name} raté !"
msgid "Missed!"
msgstr "Raté !"
msgid "Good friends are a blessing!"
msgstr "Les bons amis sont une bénédiction !"
msgid "I love my friends!"
msgstr "J'aime mes amis !"
msgid "Nobody wants to play with me ..."
msgstr "Personne ne veut jouer avec moi..."
@@ -120,11 +147,11 @@ msgid "Napping for {secs}s ..."
msgstr "Fais la sieste pendant {secs}s..."
msgid "Zzzzz"
msgstr ""
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr ""
msgstr "ZzzZzzz ({secs}s)"
msgid "Good night."
msgstr "Bonne nuit."
@@ -181,18 +208,18 @@ msgstr "{num} stations kick\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Fait {num} nouveaux amis\n"
msgstr "A {num} nouveaux amis\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Récupéré {num} handshakes\n"
msgstr "A {num} handshakes\n"
msgid "Met 1 peer"
msgstr "1 peer rencontré"
msgstr "1 pair rencontré"
#, python-brace-format
msgid "Met {num} peers"
msgstr "{num} peers recontrés"
msgstr "{num} pairs recontrés"
#, python-brace-format
msgid ""

Binary file not shown.

View File

@@ -0,0 +1,249 @@
# Pwnagotchi translation.
# Copyright (C) 2019
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR <radu.ungureanu@techie.com>, 2019.
#
#,
msgid ""
msgstr ""
"Project-Id-Version: 0.0.1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"PO-Revision-Date: 2019-11-20 00:18+594\n"
"Last-Translator: Ungureanu Radu-Andrei <radu.ungureanu@techie.com>\n"
"Language-Team: pwnagotchi <33197631+dadav@users.noreply.github."
"com>\n"
"Language: ro\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "ZzzzZZzzzzZzzz"
msgstr ""
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Buna, sunt Pwnagotchi! Pornesc..."
msgid "New day, new hunt, new pwns!"
msgstr "O noua zi, o noua vanatoare, noi pwn-uri!"
msgid "Hack the Planet!"
msgstr "Pirateaza planeta!"
msgid "AI ready."
msgstr "AI-ul e gata."
msgid "The neural network is ready."
msgstr "Rețeaua neuronală este gata."
msgid "Generating keys, do not turn off ..."
msgstr "Se generează chei, nu închide..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Hey, canalul {channel} este liber! AP-ul tău îti va mulțumi."
msgid "Reading last session logs ..."
msgstr "Se citesc log-urile din sesiunea anterioara..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Am citit {lines_so_far} linii din log pana acum..."
msgid "I'm bored ..."
msgstr "Sunt plictisit..."
msgid "Let's go for a walk!"
msgstr "Hai să ne plimbăm!"
msgid "This is the best day of my life!"
msgstr "Asta este cea mai buna zi din viața mea!"
msgid "Shitty day :/"
msgstr "O zi proasta :/"
msgid "I'm extremely bored ..."
msgstr "Sunt extrem de plictisit..."
msgid "I'm very sad ..."
msgstr "Sunt foarte trist..."
msgid "I'm sad"
msgstr "Sunt trist"
msgid "Leave me alone ..."
msgstr "Lasă-mă in pace..."
msgid "I'm mad at you!"
msgstr "Sunt supărat pe tine!"
msgid "I'm living the life!"
msgstr "Trăiesc viața!"
msgid "I pwn therefore I am."
msgstr "Eu pwn-ez, deci aici sunt."
msgid "So many networks!!!"
msgstr "Atât de multe rețele!"
msgid "I'm having so much fun!"
msgstr "Mă distrez așa de mult!"
msgid "My crime is that of curiosity ..."
msgstr "Crima mea este una de curiozitate..."
#, python-brace-format
msgid "Hello {name}! Nice to meet you."
msgstr "Bună {name}! Mă bucur să te cunosc."
#, python-brace-format
msgid "Yo {name}! Sup?"
msgstr "Yo {name}! Cmf?"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Hey {nume} ce mai faci?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Unitatea {name} este aproape!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
msgstr "Uhm... Pa {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} a dispărut."
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Oops... {name} a dispărut."
#, python-brace-format
msgid "{name} missed!"
msgstr "{name} ratat!"
msgid "Missed!"
msgstr "Ratat!"
msgid "Good friends are a blessing!"
msgstr "Prietenii buni sunt o binecuvântare!"
msgid "I love my friends!"
msgstr "Îmi iubesc prietenii!"
msgid "Nobody wants to play with me ..."
msgstr "Nimeni nu vrea sa se joace cu mine..."
msgid "I feel so alone ..."
msgstr "Mă simt așa de singuratic..."
msgid "Where's everybody?!"
msgstr "Unde-i toată lumea?!"
#, python-brace-format
msgid "Napping for {secs}s ..."
msgstr "Dorm pentru {secs}s..."
msgid "Zzzzz"
msgstr "Zzzzz"
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}s)"
msgid "Good night."
msgstr "Noapte bună."
msgid "Zzz"
msgstr "Zzz"
#, python-brace-format
msgid "Waiting for {secs}s ..."
msgstr "Aștept pentru {secs}s..."
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Mă uit împrejur ({secs}s)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
msgstr "Hey {what} hai să fim prieteni!"
#, python-brace-format
msgid "Associating to {what}"
msgstr "Mă asociez cu {what}"
#, python-brace-format
msgid "Yo {what}!"
msgstr "Yo {what}"
#, python-brace-format
msgid "Just decided that {mac} needs no WiFi!"
msgstr "Am decis că lui {mac} nu-i trebuie WiFi!"
#, python-brace-format
msgid "Deauthenticating {mac}"
msgstr "Îl deautentific pe {mac}"
#, python-brace-format
msgid "Kickbanning {mac}!"
msgstr "Îi dau kickban lui {mac}"
#, python-brace-format
msgid "Cool, we got {num} new handshake{plural}!"
msgstr "Șmecher, avem {num} de handshake-uri noi!"
#, python-brace-format
msgid "You have {count} new message{plural}!"
msgstr "Ai {count} mesaj(e) nou/noi!"
msgid "Ops, something went wrong ... Rebooting ..."
msgstr "OOps, ceva s-a întamplat... Îmi dau reboot...+"
#, python-brace-format
msgid "Kicked {num} stations\n"
msgstr "Am dat afară {num} de stații\n"
#, python-brace-format
msgid "Made {num} new friends\n"
msgstr "Am făcut {num} prieteni noi \n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Am primit {num} de handshake-uri\n"
msgid "Met 1 peer"
msgstr "Am întalnit un peer"
#, python-brace-format
msgid "Met {num} peers"
msgstr "Am întalnit {num} de peer-uri"
#, python-brace-format
msgid ""
"I've been pwning for {duration} and kicked {deauthed} clients! I've also met "
"{associated} new friends and ate {handshakes} handshakes! #pwnagotchi "
"#pwnlog #pwnlife #hacktheplanet #skynet"
msgstr "Eu am făcut pwning pentru {duration} și am dat afara {deauthed} clienți! "
"De asemenea, am întalnit {associated} prieteni noi și am mancat {handshakes} de "
"handshake-uri! #pwnagotchi #pwnlog #pwnlife #hacktheplanet #skynet"
msgid "hours"
msgstr "ore"
msgid "minutes"
msgstr "minute"
msgid "seconds"
msgstr "secunde"
msgid "hour"
msgstr "oră"
msgid "minute"
msgstr "minut"
msgid "second"
msgstr "secundă"

View File

@@ -1,45 +1,51 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Pwnagotchi Russian translation.
# Copyright (C) 2019
# This file is distributed under the same license as the pwnagotchi package.
# FIRST AUTHOR <25989971+adolfaka@users.noreply.github.com>, 2019.
#
# Second author <https://github.com/mbgroot>, 2019
msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-10-03 16:47+0200\n"
"PO-Revision-Date: 2019-10-05 18:50+0300\n"
"Language-Team: \n"
"Project-Id-Version: 0.0.2"
"Report-Msgid-Bugs-To: m-b-g@yandex.ru"
"POT-Creation-Date: 2019-11-27 16:47+0200"
"PO-Revision-Date: 2019-11-27 18:50+0300"
"Last-Translator: Evgeny Zelenin <m-b-g@yandex.ru>"
"Language-Team: ru"
"Language: ru"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.4\n"
"Last-Translator: Elliot Manson\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n"
"Language: ru_RU\n"
msgid "ZzzzZZzzzzZzzz"
msgstr "ZzzzZZzzzzZzzz"
msgstr "Хрррр..."
msgid "Hi, I'm Pwnagotchi! Starting ..."
msgstr "Привет, я Pwnagotchi! Поехали …"
msgstr "Привет, я Pwnagotchi! Стартуем!"
msgid "New day, new hunt, new pwns!"
msgstr "Новый день, новая охота, новые взломы!"
msgid "Hack the Planet!"
msgstr "Хак зе планет!"
msgstr "Взломай эту Планету!"
msgid "AI ready."
msgstr "AI готов."
msgstr "A.I. готов."
msgid "The neural network is ready."
msgstr "Нейронная сеть готова."
msgid "Generating keys, do not turn off ..."
msgstr "Генерация ключей, не выключайте..."
#, python-brace-format
msgid "Hey, channel {channel} is free! Your AP will say thanks."
msgstr "Эй, канал {channel} свободен! Ваша точка доступа скажет спасибо."
msgid "Reading last session logs ..."
msgstr "Чтение логов последнего сеанса..."
#, python-brace-format
msgid "Read {lines_so_far} log lines so far ..."
msgstr "Чтение {lines_so_far} строк журнала..."
msgid "I'm bored ..."
msgstr "Мне скучно …"
@@ -61,9 +67,14 @@ msgstr "Мне очень грустно …"
msgid "I'm sad"
msgstr "Мне грустно"
msgid "Leave me alone ..."
msgstr "Оставь меня в покое..."
msgid "I'm mad at you!"
msgstr "Я зол на тебя!"
msgid "I'm living the life!"
msgstr "Угараю по полной!"
msgstr "Живу полной жизнью!"
msgid "I pwn therefore I am."
msgstr "Я взламываю, поэтому я существую."
@@ -75,15 +86,22 @@ msgid "I'm having so much fun!"
msgstr "Мне так весело!"
msgid "My crime is that of curiosity ..."
msgstr "Моe преступление - это любопытство …"
msgstr "Моё преступление - это любопытство…"
#, python-brace-format
msgid "Hello {name}! Nice to meet you. {name}"
msgstr "Привет, {name}! Приятно познакомиться. {name}"
msgstr "Привет, {name}! Рад встрече с тобой!"
#, python-brace-format
msgid "Unit {name} is nearby! {name}"
msgstr "Цель {name} близко! {name}"
msgstr "Цель {name} близко!"
#, python-brace-format
msgid "Hey {name} how are you doing?"
msgstr "Хэй {nume}! Как дела?"
#, python-brace-format
msgid "Unit {name} is nearby!"
msgstr "Цель {name} рядом!"
#, python-brace-format
msgid "Uhm ... goodbye {name}"
@@ -91,11 +109,11 @@ msgstr "Хм … до свидания {name}"
#, python-brace-format
msgid "{name} is gone ..."
msgstr "{name} исчезла …"
msgstr "{name} ушла…"
#, python-brace-format
msgid "Whoops ... {name} is gone."
msgstr "Упс … {name} исчезла."
msgstr "Упс… {name} исчезла."
#, python-brace-format
msgid "{name} missed!"
@@ -103,12 +121,17 @@ msgstr "{name} упустил!"
msgid "Missed!"
msgstr "Промахнулся!"
msgid "Good friends are a blessing!"
msgstr "Хорошие друзья - это благословение!"
msgid "I love my friends!"
msgstr "Я люблю своих друзей!"
msgid "Nobody wants to play with me ..."
msgstr "Никто не хочет со мной играть ..."
msgid "I feel so alone ..."
msgstr "Мне так одиноко …"
msgstr "Я так одинок…"
msgid "Where's everybody?!"
msgstr "Где все?!"
@@ -118,11 +141,16 @@ msgid "Napping for {secs}s ..."
msgstr "Дремлет {secs}с …"
msgid "Zzzzz"
msgstr "Zzzzz"
msgstr "Хррр..."
#, python-brace-format
msgid "ZzzZzzz ({secs}s)"
msgstr "ZzzZzzz ({secs}c)"
msgstr "Хррррр.. ({secs}c)"
msgid "Good night."
msgstr "Доброй ночи."
msgid "Zzz"
msgstr "Хрррр"
#, python-brace-format
msgid "Waiting for {secs}s ..."
@@ -130,7 +158,7 @@ msgstr "Ждем {secs}c …"
#, python-brace-format
msgid "Looking around ({secs}s)"
msgstr "Оглядываюсь вокруг ({secs}с)"
msgstr "Осматриваюсь вокруг ({secs}с)"
#, python-brace-format
msgid "Hey {what} let's be friends!"
@@ -173,7 +201,7 @@ msgstr "Заимел {num} новых друзей\n"
#, python-brace-format
msgid "Got {num} handshakes\n"
msgstr "Получил {num} рукопожатие\n"
msgstr "Получил {num} рукопожатий\n"
msgid "Met 1 peer"
msgstr "Встретился один знакомый"

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-04 12:57+0100\n"
"POT-Creation-Date: 2019-11-29 21:50+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"

View File

@@ -3,10 +3,11 @@ import glob
import _thread
import importlib, importlib.util
import logging
from pwnagotchi.ui import view
default_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "default")
loaded = {}
database = {}
class Plugin:
@classmethod
@@ -18,6 +19,28 @@ class Plugin:
logging.debug("loaded plugin %s as %s" % (plugin_name, plugin_instance))
loaded[plugin_name] = plugin_instance
def toggle_plugin(name, enable=True):
"""
Load or unload a plugin
returns True if changed, otherwise False
"""
global loaded, database
if not enable and name in loaded:
if getattr(loaded[name], 'on_unload', None):
loaded[name].on_unload(view.ROOT)
del loaded[name]
return True
if enable and name in database and name not in loaded:
load_from_file(database[name])
one(name, 'loaded')
one(name, 'ui_setup', view.ROOT)
one(name, 'ready', view.ROOT._agent)
return True
return False
def on(event_name, *args, **kwargs):
for plugin_name, plugin in loaded.items():
@@ -48,10 +71,11 @@ def load_from_file(filename):
def load_from_path(path, enabled=()):
global loaded
global loaded, database
logging.debug("loading plugins from %s - enabled: %s" % (path, enabled))
for filename in glob.glob(os.path.join(path, "*.py")):
plugin_name = os.path.basename(filename.replace(".py", ""))
database[plugin_name] = filename
if plugin_name in enabled:
try:
load_from_file(filename)

View File

@@ -2,6 +2,7 @@ import logging
import os
import subprocess
import time
from threading import Lock
import dbus
@@ -426,6 +427,7 @@ class BTTether(plugins.Plugin):
self.ready = False
self.options = dict()
self.devices = dict()
self.lock = Lock()
def on_loaded(self):
# new config
@@ -466,110 +468,116 @@ class BTTether(plugins.Plugin):
logging.info("BT-TETHER: Successfully loaded ...")
self.ready = True
def on_unload(self, ui):
with ui._lock:
ui.remove_element('bluetooth')
def on_ui_setup(self, ui):
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
with ui._lock:
ui.add_element('bluetooth', LabeledValue(color=BLACK, label='BT', value='-', position=(ui.width() / 2 - 15, 0),
label_font=fonts.Bold, text_font=fonts.Medium))
def on_ui_update(self, ui):
if not self.ready:
return
devices_to_try = list()
connected_priorities = list()
any_device_connected = False # if this is true, last status on screen should be C
with self.lock:
devices_to_try = list()
connected_priorities = list()
any_device_connected = False # if this is true, last status on screen should be C
for _, device in self.devices.items():
if device.connected():
connected_priorities.append(device.priority)
any_device_connected = True
continue
for _, device in self.devices.items():
if device.connected():
connected_priorities.append(device.priority)
any_device_connected = True
continue
if not device.max_tries or (device.max_tries > device.tries):
if not device.status.newer_then_minutes(device.interval):
devices_to_try.append(device)
device.status.update()
device.tries += 1
if not device.max_tries or (device.max_tries > device.tries):
if not device.status.newer_then_minutes(device.interval):
devices_to_try.append(device)
device.status.update()
device.tries += 1
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
sorted_devices = sorted(devices_to_try, key=lambda x: x.search_order)
for device in sorted_devices:
bt = BTNap(device.mac)
for device in sorted_devices:
bt = BTNap(device.mac)
try:
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
dev_remote = bt.wait_for_device(timeout=device.scantime)
if dev_remote is None:
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
try:
logging.debug('BT-TETHER: Search %d secs for %s ...', device.scantime, device.name)
dev_remote = bt.wait_for_device(timeout=device.scantime)
if dev_remote is None:
logging.debug('BT-TETHER: Could not find %s, try again in %d minutes.', device.name, device.interval)
ui.set('bluetooth', 'NF')
continue
except Exception as bt_ex:
logging.error(bt_ex)
ui.set('bluetooth', 'NF')
continue
except Exception as bt_ex:
logging.error(bt_ex)
ui.set('bluetooth', 'NF')
continue
paired = bt.is_paired()
if not paired:
if BTNap.pair(dev_remote):
logging.debug('BT-TETHER: Paired with %s.', device.name)
paired = bt.is_paired()
if not paired:
if BTNap.pair(dev_remote):
logging.debug('BT-TETHER: Paired with %s.', device.name)
else:
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
ui.set('bluetooth', 'PE')
continue
else:
logging.debug('BT-TETHER: Pairing with %s failed ...', device.name)
ui.set('bluetooth', 'PE')
continue
else:
logging.debug('BT-TETHER: Already paired.')
logging.debug('BT-TETHER: Already paired.')
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
device.network, success = BTNap.nap(dev_remote)
interface = None
logging.debug('BT-TETHER: Try to create nap connection with %s ...', device.name)
device.network, success = BTNap.nap(dev_remote)
interface = None
if success:
try:
interface = device.interface()
except Exception:
if success:
try:
interface = device.interface()
except Exception:
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
continue
if interface is None:
ui.set('bluetooth', 'BE')
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
continue
logging.debug('BT-TETHER: Created interface (%s)', interface)
ui.set('bluetooth', 'C')
any_device_connected = True
device.tries = 0 # reset tries
else:
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
ui.set('bluetooth', 'NF')
continue
if interface is None:
ui.set('bluetooth', 'BE')
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
addr = f"{device.ip}/{device.netmask}"
if device.gateway:
gateway = device.gateway
else:
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
wrapped_interface = IfaceWrapper(interface)
logging.debug('BT-TETHER: Add ip to %s', interface)
if not wrapped_interface.set_addr(addr):
ui.set('bluetooth', 'AE')
logging.debug("BT-TETHER: Could not add ip to %s", interface)
continue
logging.debug('BT-TETHER: Created interface (%s)', interface)
if device.share_internet:
if not connected_priorities or device.priority > max(connected_priorities):
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
IfaceWrapper.set_route(gateway, interface)
connected_priorities.append(device.priority)
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
with open('/etc/resolv.conf', 'r+') as resolv:
nameserver = resolv.read()
if 'nameserver 9.9.9.9' not in nameserver:
logging.debug('BT-TETHER: Added nameserver')
resolv.seek(0)
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
if any_device_connected:
ui.set('bluetooth', 'C')
any_device_connected = True
device.tries = 0 # reset tries
else:
logging.debug('BT-TETHER: Could not establish nap connection with %s', device.name)
ui.set('bluetooth', 'NF')
continue
addr = f"{device.ip}/{device.netmask}"
if device.gateway:
gateway = device.gateway
else:
gateway = ".".join(device.ip.split('.')[:-1] + ['1'])
wrapped_interface = IfaceWrapper(interface)
logging.debug('BT-TETHER: Add ip to %s', interface)
if not wrapped_interface.set_addr(addr):
ui.set('bluetooth', 'AE')
logging.debug("BT-TETHER: Could not add ip to %s", interface)
continue
if device.share_internet:
if not connected_priorities or device.priority > max(connected_priorities):
logging.debug('BT-TETHER: Set default route to %s via %s', gateway, interface)
IfaceWrapper.set_route(gateway, interface)
connected_priorities.append(device.priority)
logging.debug('BT-TETHER: Change resolv.conf if necessary ...')
with open('/etc/resolv.conf', 'r+') as resolv:
nameserver = resolv.read()
if 'nameserver 9.9.9.9' not in nameserver:
logging.debug('BT-TETHER: Added nameserver')
resolv.seek(0)
resolv.write(nameserver + 'nameserver 9.9.9.9\n')
if any_device_connected:
ui.set('bluetooth', 'C')

View File

@@ -25,6 +25,10 @@ class Example(plugins.Plugin):
def on_loaded(self):
logging.warning("WARNING: this plugin should be disabled! options = " % self.options)
# called before the plugin is unloaded
def on_unload(self, ui):
pass
# called hen there's internet connectivity
def on_internet_available(self, agent):
pass
@@ -118,6 +122,11 @@ class Example(plugins.Plugin):
def on_wifi_update(self, agent, access_points):
pass
# called when the agent refreshed an unfiltered access point list
# this list contains all access points that were detected BEFORE filtering
def on_unfiltered_ap_list(self, agent, access_points):
pass
# called when the agent is sending an association frame
def on_association(self, agent, access_point):
pass

View File

@@ -32,6 +32,7 @@ class GPIOButtons(plugins.Plugin):
GPIO.setmode(GPIO.BCM)
for gpio, command in gpios.items():
gpio = int(gpio)
self.ports[gpio] = command
GPIO.setup(gpio, GPIO.IN, GPIO.PUD_UP)
GPIO.add_event_detect(gpio, GPIO.FALLING, callback=self.runCommand, bouncetime=600)

View File

@@ -59,6 +59,11 @@ class GPS(plugins.Plugin):
lat_pos = (112, 30)
lon_pos = (112, 49)
alt_pos = (87, 63)
elif ui.is_waveshare144lcd():
# guessed values, add tested ones if you can
lat_pos = (67, 73)
lon_pos = (62, 83)
alt_pos = (67, 93)
else:
# guessed values, add tested ones if you can
lat_pos = (127, 51)

View File

@@ -67,7 +67,8 @@ class Grid(plugins.Plugin):
logging.info("grid plugin loaded.")
def set_reported(self, reported, net_id):
reported.append(net_id)
if net_id not in reported:
reported.append(net_id)
self.report.update(data={'reported': reported})
def check_inbox(self, agent):

View File

@@ -44,12 +44,12 @@ class Led(plugins.Plugin):
logging.debug("[led] using pattern '%s' ..." % pattern)
for c in pattern:
if c == ' ':
self._led(0)
else:
self._led(1)
else:
self._led(0)
time.sleep(self._delay / 1000.0)
# reset
self._led(1)
self._led(0)
def _worker(self):
while True:

View File

@@ -44,6 +44,9 @@ class MemTemp(plugins.Plugin):
if ui.is_waveshare_v2():
h_pos = (180, 80)
v_pos = (180, 61)
elif ui.is_waveshare144lcd():
h_pos = (53, 77)
v_pos = (78, 67)
elif ui.is_inky():
h_pos = (140, 68)
v_pos = (165, 54)

View File

@@ -1,6 +1,7 @@
import logging
import json
import os
import threading
import requests
import time
import pwnagotchi.plugins as plugins
@@ -11,7 +12,7 @@ MOZILLA_API_URL = 'https://location.services.mozilla.com/v1/geolocate?key={api}'
class NetPos(plugins.Plugin):
__author__ = 'zenzen san'
__version__ = '2.0.1'
__version__ = '2.0.2'
__license__ = 'GPL3'
__description__ = """Saves a json file with the access points with more signal
whenever a handshake is captured.
@@ -22,6 +23,7 @@ class NetPos(plugins.Plugin):
self.report = StatusFile('/root/.net_pos_saved', data_format='json')
self.skip = list()
self.ready = False
self.lock = threading.Lock()
def on_loaded(self):
if 'api_key' not in self.options or ('api_key' in self.options and self.options['api_key'] is None):
@@ -45,60 +47,64 @@ class NetPos(plugins.Plugin):
saved_file.write(x + "\n")
def on_internet_available(self, agent):
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
with self.lock:
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
all_files = os.listdir(handshake_dir)
all_np_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.net-pos.json')]
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
all_files = os.listdir(handshake_dir)
all_np_files = [os.path.join(handshake_dir, filename)
for filename in all_files
if filename.endswith('.net-pos.json')]
new_np_files = set(all_np_files) - set(reported) - set(self.skip)
if new_np_files:
logging.info("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
display.update(force=True)
for idx, np_file in enumerate(new_np_files):
if new_np_files:
logging.debug("NET-POS: Found %d new net-pos files. Fetching positions ...", len(new_np_files))
display.set('status', f"Found {len(new_np_files)} new net-pos files. Fetching positions ...")
display.update(force=True)
for idx, np_file in enumerate(new_np_files):
geo_file = np_file.replace('.net-pos.json', '.geo.json')
if os.path.exists(geo_file):
# got already the position
reported.append(np_file)
self.report.update(data={'reported': reported})
continue
try:
geo_data = self._get_geo_data(np_file) # returns json obj
except requests.exceptions.RequestException as req_e:
logging.error("NET-POS: %s - RequestException: %s", np_file, req_e)
self.skip += np_file
continue
except json.JSONDecodeError as js_e:
logging.error("NET-POS: %s - JSONDecodeError: %s, removing it...", np_file, js_e)
os.remove(np_file)
continue
except OSError as os_e:
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
self.skip += np_file
continue
with open(geo_file, 'w+t') as sf:
json.dump(geo_data, sf)
geo_file = np_file.replace('.net-pos.json', '.geo.json')
if os.path.exists(geo_file):
# got already the position
reported.append(np_file)
self.report.update(data={'reported': reported})
continue
try:
geo_data = self._get_geo_data(np_file) # returns json obj
except requests.exceptions.RequestException as req_e:
logging.error("NET-POS: %s - RequestException: %s", np_file, req_e)
self.skip += np_file
continue
except json.JSONDecodeError as js_e:
logging.error("NET-POS: %s - JSONDecodeError: %s", np_file, js_e)
self.skip += np_file
continue
except OSError as os_e:
logging.error("NET-POS: %s - OSError: %s", np_file, os_e)
self.skip += np_file
continue
with open(geo_file, 'w+t') as sf:
json.dump(geo_data, sf)
reported.append(np_file)
self.report.update(data={'reported': reported})
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
display.update(force=True)
display.set('status', f"Fetching positions ({idx + 1}/{len(new_np_files)})")
display.update(force=True)
def on_handshake(self, agent, filename, access_point, client_station):
netpos = self._get_netpos(agent)
if not netpos['wifiAccessPoints']:
return
netpos["ts"] = int("%.0f" % time.time())
netpos_filename = filename.replace('.pcap', '.net-pos.json')
logging.info("NET-POS: Saving net-location to %s", netpos_filename)
logging.debug("NET-POS: Saving net-location to %s", netpos_filename)
try:
with open(netpos_filename, 'w+t') as net_pos_file:
@@ -106,6 +112,7 @@ class NetPos(plugins.Plugin):
except OSError as os_e:
logging.error("NET-POS: %s", os_e)
def _get_netpos(self, agent):
aps = agent.get_access_points()
netpos = dict()

View File

@@ -2,20 +2,27 @@ import os
import logging
import re
import requests
from threading import Lock
from pwnagotchi.utils import StatusFile
import pwnagotchi.plugins as plugins
from json.decoder import JSONDecodeError
class OnlineHashCrack(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '2.0.0'
__version__ = '2.0.1'
__license__ = 'GPL3'
__description__ = 'This plugin automatically uploads handshakes to https://onlinehashcrack.com'
def __init__(self):
self.ready = False
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
try:
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
except JSONDecodeError as json_err:
os.remove('/root/.ohc_uploads')
self.report = StatusFile('/root/.ohc_uploads', data_format='json')
self.skip = list()
self.lock = Lock()
def on_loaded(self):
"""
@@ -68,38 +75,40 @@ class OnlineHashCrack(plugins.Plugin):
"""
Called in manual mode when there's internet connectivity
"""
if self.ready:
display = agent.view()
config = agent.config()
reported = self.report.data_field_or('reported', default=list())
with self.lock:
if self.ready:
display = agent.view()
config = agent.config()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
# pull out whitelisted APs
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
# pull out whitelisted APs
handshake_paths = filter(lambda path: self._filter_handshake_file(path), handshake_paths)
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
if handshake_new:
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
if handshake_new:
logging.info("OHC: Internet connectivity detected. Uploading new handshakes to onelinehashcrack.com")
for idx, handshake in enumerate(handshake_new):
display.set('status',
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_ohc(handshake)
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info(f"OHC: Successfully uploaded {handshake}")
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("OHC: %s", req_e)
continue
except OSError as os_e:
self.skip.append(handshake)
logging.error("OHC: %s", os_e)
continue
for idx, handshake in enumerate(handshake_new):
display.set('status',
f"Uploading handshake to onlinehashcrack.com ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_ohc(handshake)
if handshake not in reported:
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info(f"OHC: Successfully uploaded {handshake}")
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("OHC: %s", req_e)
continue
except OSError as os_e:
self.skip.append(handshake)
logging.error("OHC: %s", os_e)
continue

View File

@@ -4,10 +4,7 @@ import pwnagotchi.plugins as plugins
'''
You need an bluetooth connection to your android phone which is running PAW server with the GPS "hack" from Systemik and edited by shaynemk
NEW BETTER GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
Old guide here, (not recommended if you plan on using it with the webgpsmap plugin)
https://raw.githubusercontent.com/systemik/pwnagotchi-bt-tether/master/GPS-via-PAW
GUIDE HERE: https://community.pwnagotchi.ai/t/setting-up-paw-gps-on-android
'''
@@ -30,7 +27,7 @@ class PawGPS(plugins.Plugin):
ip = self.options['ip']
gps = requests.get('http://' + ip + '/gps.xhtml')
gps_filename = filename.replace('.pcap', '.gps.json')
gps_filename = filename.replace('.pcap', '.paw-gps.json')
logging.info("saving GPS to %s (%s)" % (gps_filename, gps))
with open(gps_filename, 'w+t') as f:

View File

@@ -0,0 +1,195 @@
import os
import logging
import threading
from datetime import datetime
from pwnagotchi import plugins
from flask import render_template_string
from flask import jsonify
TEMPLATE = """
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
Session stats
{% endblock %}
{% block styles %}
<link rel="stylesheet" href="/css/jquery.jqplot.min.css"/>
<link rel="stylesheet" href="/css/jquery.jqplot.css"/>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="/js/jquery.jqplot.min.js"></script>
<script type="text/javascript" src="/js/jquery.jqplot.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.mobile.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.json2.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.dateAxisRenderer.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.highlighter.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.cursor.js"></script>
<script type="text/javascript" src="/js/plugins/jqplot.enhancedLegendRenderer.js"></script>
{% endblock %}
{% block script %}
$(document).ready(function(){
var ajaxDataRenderer = function(url, plot, options) {
var ret = null;
$.ajax({
async: false,
url: url,
dataType:"json",
success: function(data) {
ret = data;
}
});
return ret;
};
function loadData(url, elm, title) {
var data = ajaxDataRenderer(url);
var plot_os = $.jqplot(elm, data.values,{
title: title,
stackSeries: true,
seriesDefaults: {
showMarker: false,
fill: true,
fillAndStroke: true
},
legend: {
show: true,
renderer: $.jqplot.EnhancedLegendRenderer,
placement: 'outsideGrid',
labels: data.labels,
location: 's',
rendererOptions: {
numberRows: '2',
},
rowSpacing: '0px'
},
axes:{
xaxis:{
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
min: 0,
tickOptions:{formatString:'%.2f'}
}
},
highlighter: {
show: true,
sizeAdjust: 7.5
},
cursor:{
show: true,
tooltipLocation:'sw'
}
}).replot({
axes:{
xaxis:{
renderer:$.jqplot.DateAxisRenderer,
tickOptions:{formatString:'%H:%M:%S'}
},
yaxis:{
min: 0,
tickOptions:{formatString:'%.2f'}
}
}
});
}
function loadAll() {
loadData('/plugins/session-stats/os', 'chart_os', 'OS')
loadData('/plugins/session-stats/temp', 'chart_temp', 'Temp')
loadData('/plugins/session-stats/nums', 'chart_nums', 'Wifi')
loadData('/plugins/session-stats/duration', 'chart_duration', 'Sleeping')
loadData('/plugins/session-stats/epoch', 'chart_epoch', 'Epochs')
}
loadAll();
setInterval(loadAll, 60000);
});
{% endblock %}
{% block content %}
<div id="chart_os" style="height:400px;width:100%; "></div>
<div id="chart_temp" style="height:400px;width:100%; "></div>
<div id="chart_nums" style="height:400px;width:100%; "></div>
<div id="chart_duration" style="height:400px;width:100%; "></div>
<div id="chart_epoch" style="height:400px;width:100%; "></div>
{% endblock %}
"""
class SessionStats(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '0.1.0'
__license__ = 'GPL3'
__description__ = 'This plugin displays stats of the current session.'
def __init__(self):
self.ready = False
self.lock = threading.Lock()
self.options = dict()
self.stats = dict()
def on_loaded(self):
"""
Gets called when the plugin gets loaded
"""
logging.info("Session-stats plugin loaded.")
self.ready = True
def on_unloaded(self, ui):
pass
def on_epoch(self, agent, epoch, epoch_data):
"""
Save the epoch_data to self.stats
"""
with self.lock:
self.stats[datetime.now().strftime("%H:%M:%S")] = epoch_data
@staticmethod
def extract_key_values(data, subkeys):
result = dict()
result['values'] = list()
result['labels'] = subkeys
for plot_key in subkeys:
v = [ [ts,d[plot_key]] for ts, d in data.items()]
result['values'].append(v)
return result
def on_webhook(self, path, request):
if not path or path == "/":
return render_template_string(TEMPLATE)
if path == "os":
extract_keys = ['cpu_load','mem_usage',]
elif path == "temp":
extract_keys = ['temperature']
elif path == "nums":
extract_keys = [
'missed_interactions',
'num_hops',
'num_peers',
'tot_bond',
'avg_bond',
'num_deauths',
'num_associations',
'num_handshakes',
]
elif path == "duration":
extract_keys = [
'duration_secs',
'slept_for_secs',
]
elif path == "epoch":
extract_keys = [
'blind_for_epochs',
'inactive_for_epochs',
'active_for_epochs',
]
with self.lock:
return jsonify(SessionStats.extract_key_values(self.stats, extract_keys))

View File

@@ -500,8 +500,9 @@ class WebConfig(plugins.Plugin):
elif request.method == "POST":
if path == "save-config":
try:
parsed_yaml = yaml.safe_load(str(request.get_json()))
with open('/etc/pwnagotchi/config.yml', 'w') as config_file:
yaml.safe_dump(request.get_json(), config_file, encoding='utf-8',
yaml.safe_dump(parsed_yaml, config_file, encoding='utf-8',
allow_unicode=True, default_flow_style=False)
_thread.start_new_thread(restart, (self.mode,))

View File

@@ -9,7 +9,7 @@
<script type='text/javascript' src='http://cdnjs.cloudflare.com/ajax/libs/leaflet.markercluster/1.4.1/leaflet.markercluster.js'></script>
<style type="text/css">
/* for map */
html, body, #mapdiv { height: 100%; width: 100%; margin:0; background-color:#000;}
html, body { height: 100%; width: 100%; margin:0; background-color:#000;}
.pwnAPPin path {
fill: #ce7575;
}
@@ -85,11 +85,19 @@
display: none;
}
#loading .face { font-size:8vw; }
#loading .text {position:absolute;bottom:0;text-align:center; font-size: 1vw;color:#a0a0a0;}
#loading .text { position:absolute;bottom:0;text-align:center; font-size: 2vw;color:#a0a0a0;}
#filterbox { position: fixed;top:0px;left:0px;z-index:999999;margin-left:55px;width:100%;height:20px;border-bottom:2px solid #303030;display: grid;grid-template-columns: 1fr 0.1fr;grid-template-rows: 1fr;grid-template-areas: ". .";}
#search { grid-area: 1 / 1 / 2 / 2;height:30px;padding:3px;background-color:#000;color:#e0e0e0;border:none;}
#matchcount { grid-area: 1 / 2 / 2 / 3;height:30px;margin-right:55px;padding-right:5px;background-color:#000;color:#a0a0a0;font-weight:bold;}
#mapdiv { width:100%; height: 100%; }
</style>
</head>
<body>
<div id="mapdiv"></div>
<div id="filterbox">
<input type="text" id="search" placeholder="filter: #cracked #notcracked AA:BB:CC aabbcc AndroidAP ..."/>
<div id="matchcount">0&nbsp;APs</div>
</div>
<div id="loading"><div class="face"><nobr>(⌐■&nbsp;<span id="loading_ap_img"></span>&nbsp;■)</nobr></div><div class="text" id="loading_infotext">loading positions...</div></div>
<script type="text/javascript">
function loadJSON(url, callback) {
@@ -133,11 +141,9 @@
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
subdomains: 'abcd',
opacity:0.8,
maxZoom: 19
// maxZoom: 19
});
var mymap = L.map('mapdiv');
Esri_WorldImagery.addTo(mymap);
CartoDB_DarkMatter.addTo(mymap);
var svg = '<svg class="pwnAPPin" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#931F1F" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#931F1F" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#931F1F" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#931F1F" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
var svgOpen = '<svg class="pwnAPPinOpen" width="80px" height="60px" viewBox="0 0 44 28" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><desc>pwnagotchi AP icon.</desc><defs><linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1"><stop stop-color="#FFFFFF" offset="0%"></stop><stop stop-color="#000000" offset="100%"></stop></linearGradient></defs><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="marker"><path class="ring_outer" d="M28.6,8 C34.7,9.4 39,12.4 39,16 C39,20.7 31.3,24.6 21.7,24.6 C12.1,24.6 4.3,20.7 4.3,16 C4.3,12.5 8.5,9.5 14.6,8.1 C15.3,8 14.2,6.6 13.3,6.8 C5.5,8.4 0,12.2 0,16.7 C0,22.7 9.7,27.4 21.7,27.4 C33.7,27.4 43.3,22.6 43.3,16.7 C43.3,12.1 37.6,8.3 29.6,6.7 C28.8,6.5 27.8,7.9 28.6,8.1 L28.6,8 Z" id="Shape" fill="#878787" fill-rule="nonzero"></path><path class="ring_inner" d="M28.1427313,11.0811939 C30.4951542,11.9119726 32.0242291,13.2174821 32.0242291,14.6416742 C32.0242291,17.2526931 27.6722467,19.2702986 22.261674,19.2702986 C16.8511013,19.2702986 12.4991189,17.2526931 12.4991189,14.7603569 C12.4991189,13.5735301 13.4400881,12.505386 15.0867841,11.6746073 C15.792511,11.3185592 14.7339207,9.30095371 13.9105727,9.77568442 C10.6171806,10.9625112 8.5,12.9801167 8.5,15.2350876 C8.5,19.0329333 14.4986784,22.0000002 21.9088106,22.0000002 C29.2013216,22.0000002 35.2,19.0329333 35.2,15.2350876 C35.2,12.861434 32.7299559,10.6064632 28.8484581,9.30095371 C28.0251101,9.18227103 27.4370044,10.8438285 28.0251101,11.0811939 L28.1427313,11.0811939 Z" id="Shape" fill="#5F5F5F" fill-rule="nonzero"></path><g id="ap" transform="translate(13.000000, 0.000000)"><rect id="apfront" fill="#000000" x="0" y="14" width="18" height="4"></rect><polygon id="apbody" fill="url(#linearGradient-1)" points="3.83034404 10 14.169656 10 18 14 0 14"></polygon><circle class="ring_outer" id="led1" fill="#1f9321" cx="3" cy="16" r="1"></circle><circle class="ring_inner" id="led2" fill="#1f9321" cx="7" cy="16" r="1"></circle><circle class="ring_outer" id="led3" fill="#1f9321" cx="11" cy="16" r="1"></circle><circle class="ring_inner" id="led4" fill="#1f9321" cx="15" cy="16" r="1"></circle><polygon id="antenna2" fill="#000000" points="8.8173082 0 9.1826918 0 9.5 11 8.5 11"></polygon><polygon id="antenna3" fill="#000000" transform="translate(15.000000, 5.500000) rotate(15.000000) translate(-15.000000, -5.500000) " points="14.8173082 0 15.1826918 0 15.5 11 14.5 11"></polygon><polygon id="antenna1" fill="#000000" transform="translate(3.000000, 5.500000) rotate(-15.000000) translate(-3.000000, -5.500000) " points="2.8173082 0 3.1826918 0 3.5 11 2.5 11"></polygon></g></g></g></svg>';
@@ -157,54 +163,88 @@
popupAnchor : [0, -30],
});
var positionsLoaded = false;
var positions = [];
var accuracys = [];
var markers = [];
var marker_pos = [];
var markerClusters = L.markerClusterGroup();
loadJSON("/plugins/webgpsmap/all", function(response) {
var positions = JSON.parse(response);
function drawPositions() {
count = 0;
//mymap.removeLayer(markerClusters);
mymap.eachLayer(function (layer) {
mymap.removeLayer(layer);
});
Esri_WorldImagery.addTo(mymap);
CartoDB_DarkMatter.addTo(mymap);
markerClusters = L.markerClusterGroup();
accuracys = [];
markers = [];
marker_pos = [];
filterText = document.getElementById("search").value;
//console.log(filterText);
Object.keys(positions).forEach(function(key) {
count++;
if(positions[key].lng){
new_marker_pos = [positions[key].lat, positions[key].lng];
if (positions[key].acc) {
radius = Math.round(Math.min(positions[key].acc, 500));
markerColor = 'red';
markerColorCode = '#f03';
fillOpacity = 0.002;
if (positions[key].pass) {
markerColor = 'green';
markerColorCode = '#1aff00';
fillOpacity = 0.1;
}
accuracys.push(
L.circle(new_marker_pos, {
color: markerColor,
fillColor: markerColorCode,
fillOpacity: fillOpacity,
weight: 1,
opacity: 0.1,
radius: Math.min(positions[key].acc, 500),
}).setStyle({'className': 'radar'}).addTo(mymap)
);
}
filterPattern =
positions[key].ssid + ' ' +
formatMacAddress(positions[key].mac) + ' ' +
positions[key].mac
;
if (positions[key].pass) {
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
filterPattern += positions[key].pass + ' #cracked';
} else {
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
filterPattern += ' #notcracked';
}
passInfo = '';
if (positions[key].pass) {
filterPattern = filterPattern.toLowerCase();
//console.log(filterPattern);
var matched = true;
if (filterText) {
filterText.split(" ").forEach(function (item) {
if (!filterPattern.includes(item.toLowerCase())) {
matched = false;
}
});
}
if (matched) {
count++;
new_marker_pos = [positions[key].lat, positions[key].lng];
if (positions[key].acc) {
radius = Math.round(Math.min(positions[key].acc, 500));
markerColor = 'red';
markerColorCode = '#f03';
fillOpacity = 0.002;
if (positions[key].pass) {
markerColor = 'green';
markerColorCode = '#1aff00';
fillOpacity = 0.1;
}
accuracys.push(
L.circle(new_marker_pos, {
color: markerColor,
fillColor: markerColorCode,
fillOpacity: fillOpacity,
weight: 1,
opacity: 0.1,
radius: Math.min(positions[key].acc, 500),
}).setStyle({'className': 'radar'}).addTo(mymap)
);
}
passInfo = '';
if (positions[key].pass) {
passInfo = '<br/><b>Pass:</b> '+escapeHtml(positions[key].pass);
newMarker = L.marker(new_marker_pos, { icon: myIconOpen, title: positions[key].ssid }); //.addTo(mymap);
} else {
newMarker = L.marker(new_marker_pos, { icon: myIcon, title: positions[key].ssid }); //.addTo(mymap);
}
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
markers.push(newMarker);
marker_pos.push(new_marker_pos);
markerClusters.addLayer( newMarker );
}
newMarker.bindPopup("<b>"+escapeHtml(positions[key].ssid)+"</b><br><nobr>MAC: "+escapeHtml(formatMacAddress(positions[key].mac))+"</nobr><br/>"+"<nobr>position type: "+escapeHtml(positions[key].type)+"</nobr><br/>"+"<nobr>position accuracy: "+escapeHtml(Math.round(positions[key].acc))+"</nobr>"+passInfo, { maxWidth: "auto" });
markers.push(newMarker);
marker_pos.push(new_marker_pos);
markerClusters.addLayer( newMarker );
}
});
document.getElementById("matchcount").innerHTML = count + "&nbsp;APs";
if (count > 0) {
mymap.addLayer( markerClusters );
var bounds = new L.LatLngBounds(marker_pos);
@@ -213,6 +253,22 @@
} else {
document.getElementById("loading_infotext").innerHTML = "NO POSITION DATA FOUND :(";
}
}
// draw map on Enter in FilterInputField
const node = document.getElementById("search").addEventListener("keyup", function(event) {
if (event.key === "Enter") {
if (positionsLoaded) {
drawPositions();
}
}
});
// load positions
loadJSON("/plugins/webgpsmap/all", function(response) {
positions = JSON.parse(response);
positionsLoaded = true;
drawPositions();
});
</script>
</body></html>
</body></html>

View File

@@ -9,7 +9,7 @@ from functools import lru_cache
'''
2do:
- make the cache handling multiple clients
- make+test the cache handling multiple clients
- cleanup the javascript in a class and handle "/newest" additions
- create map filters (only cracked APs, only last xx days, between 2 days with slider)
http://www.gistechsolutions.com/leaflet/DEMO/filter/filter.html
@@ -22,16 +22,10 @@ from functools import lru_cache
class Webgpsmap(plugins.Plugin):
__author__ = 'https://github.com/xenDE and https://github.com/dadav'
__version__ = '1.2.2'
__version__ = '1.3.0'
__name__ = 'webgpsmap'
__license__ = 'GPL3'
__description__ = 'a plugin for pwnagotchi that shows a openstreetmap with positions of ap-handshakes in your webbrowser'
__help__ = """
- install: copy "webgpsmap.py" and "webgpsmap.html" to your configured "custom_plugins" directory
- add webgpsmap.yml to your config
- connect your PC/Smartphone/* with USB, BT or other to your pwnagotchi and browse to http://pwnagotchi.local:8080/plugins/webgpsmap/
(change pwnagotchi.local to your pwnagotchis IP, if needed)
"""
ALREADY_SENT = list()
SKIP = list()
@@ -47,7 +41,7 @@ class Webgpsmap(plugins.Plugin):
"""
Plugin got loaded
"""
logging.info("webgpsmap plugin loaded")
logging.info("[webgpsmap]: plugin loaded")
def on_webhook(self, path, request):
"""
@@ -68,8 +62,8 @@ class Webgpsmap(plugins.Plugin):
response_status = 500
response_mimetype = "application/xhtml+xml"
response_header_contenttype = 'text/html'
except Exception as ex:
logging.error(ex)
except Exception as error:
logging.error(f"[webgpsmap] error: {error}")
return
else:
if request.method == "GET":
@@ -78,8 +72,8 @@ class Webgpsmap(plugins.Plugin):
self.ALREADY_SENT = list()
try:
response_data = bytes(self.get_html(), "utf-8")
except Exception as ex:
logging.error(ex)
except Exception as error:
logging.error(f"[webgpsmap] error: {error}")
return
response_status = 200
response_mimetype = "application/xhtml+xml"
@@ -92,8 +86,8 @@ class Webgpsmap(plugins.Plugin):
response_status = 200
response_mimetype = "application/json"
response_header_contenttype = 'application/json'
except Exception as ex:
logging.error(ex)
except Exception as error:
logging.error(f"[webgpsmap] error: {error}")
return
# elif path.startswith('/newest'):
# # returns all positions newer then timestamp
@@ -118,7 +112,7 @@ class Webgpsmap(plugins.Plugin):
<meta charset="utf-8"/>
<style>body{font-size:1000%;}</style>
</head>
<body>4😋4</body>
<body>4😋4 for bad boys</body>
</html>''', "utf-8")
response_status = 404
try:
@@ -126,12 +120,12 @@ class Webgpsmap(plugins.Plugin):
if response_header_contenttype is not None:
r.headers["Content-Type"] = response_header_contenttype
return r
except Exception as ex:
logging.error(ex)
except Exception as error:
logging.error(f"[webgpsmap] error: {error}")
return
# cache 1024 items
@lru_cache(maxsize=1024, typed=False)
# cache 2048 items
@lru_cache(maxsize=2048, typed=False)
def _get_pos_from_file(self, path):
return PositionFile(path)
@@ -144,7 +138,7 @@ class Webgpsmap(plugins.Plugin):
handshake_dir = gpsdir
gps_data = dict()
logging.info("webgpsmap: scanning %s", handshake_dir)
logging.info(f"[webgpsmap] scanning {handshake_dir}")
all_files = os.listdir(handshake_dir)
@@ -156,33 +150,40 @@ class Webgpsmap(plugins.Plugin):
all_geo_or_gps_files = []
for filename_pcap in all_pcap_files:
filename_base = filename_pcap[:-5] # remove ".pcap"
logging.debug("webgpsmap: found: " + filename_base)
logging.debug(f"[webgpsmap] found: {filename_base}")
filename_position = None
logging.debug("[webgpsmap] search for .gps.json")
check_for = os.path.basename(filename_base) + ".gps.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
logging.debug("[webgpsmap] search for .geo.json")
check_for = os.path.basename(filename_base) + ".geo.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
logging.debug("[webgpsmap] search for .paw-gps.json")
check_for = os.path.basename(filename_base) + ".paw-gps.json"
if check_for in all_files:
filename_position = str(os.path.join(handshake_dir, check_for))
logging.debug(f"[webgpsmap] end search for position data files and use {filename_position}")
if filename_position is not None:
# logging.debug("webgpsmap: -- found: %s %d" % (check_for, len(all_geo_or_gps_files)) )
all_geo_or_gps_files.append(filename_position)
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skiped networks? No!
# all_geo_or_gps_files = set(all_geo_or_gps_files) - set(SKIP) # remove skipped networks? No!
if newest_only:
all_geo_or_gps_files = set(all_geo_or_gps_files) - set(self.ALREADY_SENT)
logging.info("webgpsmap: Found %d .(geo|gps).json files from %d handshakes. Fetching positions ...",
len(all_geo_or_gps_files), len(all_pcap_files))
logging.info(f"[webgpsmap] Found {len(all_geo_or_gps_files)} position-data files from {len(all_pcap_files)} handshakes. Fetching positions ...")
for pos_file in all_geo_or_gps_files:
try:
pos = self._get_pos_from_file(pos_file)
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO:
if not pos.type() == PositionFile.GPS and not pos.type() == PositionFile.GEO and not pos.type() == PositionFile.PAWGPS:
continue
ssid, mac = pos.ssid(), pos.mac()
@@ -190,10 +191,17 @@ class Webgpsmap(plugins.Plugin):
# invalid mac is strange and should abort; ssid is ok
if not mac:
raise ValueError("Mac can't be parsed from filename")
pos_type = 'unknown'
if pos.type() == PositionFile.GPS:
pos_type = 'gps'
elif pos.type() == PositionFile.GEO:
pos_type = 'geo'
elif pos.type() == PositionFile.PAWGPS:
pos_type = 'paw'
gps_data[ssid+"_"+mac] = {
'ssid': ssid,
'mac': mac,
'type': 'gps' if pos.type() == PositionFile.GPS else 'geo',
'type': pos_type,
'lng': pos.lng(),
'lat': pos.lat(),
'acc': pos.accuracy(),
@@ -201,24 +209,25 @@ class Webgpsmap(plugins.Plugin):
'ts_last': pos.timestamp_last(),
}
# get ap password if exist
check_for = os.path.basename(pos_file[:-9]) + ".pcap.cracked"
if check_for in all_files:
gps_data[ssid + "_" + mac]["pass"] = pos.password()
self.ALREADY_SENT += pos_file
except json.JSONDecodeError as js_e:
except json.JSONDecodeError as error:
self.SKIP += pos_file
logging.error(js_e)
logging.error(f"[webgpsmap] JSONDecodeError in: {error}")
continue
except ValueError as v_e:
except ValueError as error:
self.SKIP += pos_file
logging.error(v_e)
logging.error(f"[webgpsmap] ValueError: {error}")
continue
except OSError as os_e:
except OSError as error:
self.SKIP += pos_file
logging.error(os_e)
logging.error(f"[webgpsmap] OSError: {error}")
continue
logging.info("webgpsmap loaded %d positions", len(gps_data))
logging.info(f"[webgpsmap] loaded {len(gps_data)} positions")
return gps_data
def get_html(self):
@@ -226,11 +235,10 @@ class Webgpsmap(plugins.Plugin):
Returns the html page
"""
try:
template_file = os.path.dirname(os.path.realpath(__file__))+"/"+"webgpsmap.html"
template_file = os.path.dirname(os.path.realpath(__file__)) + "/" + "webgpsmap.html"
html_data = open(template_file, "r").read()
except Exception as ex:
logging.error("error loading template file: %s", template_file)
logging.error(ex)
except Exception as error:
logging.error(f"[webgpsmap] error loading template file {template_file} - error: {error}")
return html_data
@@ -238,15 +246,18 @@ class PositionFile:
"""
Wraps gps / net-pos files
"""
GPS = 0
GEO = 1
GPS = 1
GEO = 2
PAWGPS = 3
def __init__(self, path):
self._file = path
self._filename = os.path.basename(path)
try:
logging.debug(f"[webgpsmap] loading {path}")
with open(path, 'r') as json_file:
self._json = json.load(json_file)
logging.debug(f"[webgpsmap] loaded {path}")
except json.JSONDecodeError as js_e:
raise js_e
@@ -254,7 +265,7 @@ class PositionFile:
"""
Returns the mac from filename
"""
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo)\.json', self._filename)
parsed_mac = re.search(r'.*_?([a-zA-Z0-9]{12})\.(?:gps|geo|paw-gps)\.json', self._filename)
if parsed_mac:
mac = parsed_mac.groups()[0]
return mac
@@ -264,7 +275,7 @@ class PositionFile:
"""
Returns the ssid from filename
"""
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo)\.json', self._filename)
parsed_ssid = re.search(r'(.+)_[a-zA-Z0-9]{12}\.(?:gps|geo|paw-gps)\.json', self._filename)
if parsed_ssid:
return parsed_ssid.groups()[0]
return None
@@ -293,32 +304,37 @@ class PositionFile:
elif 'Updated' in self._json:
# convert gps datetime to unix timestamp: "2019-10-05T23:12:40.422996+01:00"
date_iso_formated = self._json['Updated']
# fill milliseconds to 6 numbers
# bad microseconds fix: fill/cut microseconds to 6 numbers
part1, part2, part3 = re.split('\.|\+', date_iso_formated)
part2 = part2.ljust(6, '0')
part2 = part2.ljust(6, '0')[:6]
# timezone fix: 0200 >>> 02:00
if len(part3) == 4:
part3 = part3[1:2].rjust(2, '0') + ':' + part3[3:4].rjust(2, '0')
date_iso_formated = part1 + "." + part2 + "+" + part3
dateObj = datetime.datetime.fromisoformat(date_iso_formated)
return_ts = int("%.0f" % dateObj.timestamp())
else:
# use file timestamp last modification of the pcap file
# use file timestamp last modification of the json file
return_ts = int("%.0f" % os.path.getmtime(self._file))
return return_ts
def password(self):
"""
returns the password from file.pcap.cracked od None
returns the password from file.pcap.cracked or None
"""
return_pass = None
password_file_path = self._file[:-9] + ".pcap.cracked"
# 2do: make better filename split/remove extension because this one has problems with "." in path
base_filename, ext1, ext2 = re.split('\.', self._file)
password_file_path = base_filename + ".pcap.cracked"
if os.path.isfile(password_file_path):
try:
password_file = open(password_file_path, 'r')
return_pass = password_file.read()
password_file.close()
except OSError as err:
print("OS error: {0}".format(err))
except OSError as error:
logging.error(f"[webgpsmap] OS error: {format(error)}")
except:
print("Unexpected error:", sys.exc_info()[0])
logging.error(f"[webgpsmap] Unexpected error: {sys.exc_info()[0]}")
raise
return return_pass
@@ -330,37 +346,57 @@ class PositionFile:
return PositionFile.GPS
if self._file.endswith('.geo.json'):
return PositionFile.GEO
if self._file.endswith('.paw-gps.json'):
return PositionFile.PAWGPS
return None
def lat(self):
try:
if self.type() == PositionFile.GPS:
lat = None
# try to get value from known formats
if 'Latitude' in self._json:
lat = self._json['Latitude']
if self.type() == PositionFile.GEO:
lat = self._json['location']['lat']
if lat != 0:
return lat
raise ValueError("Lat is 0")
if 'lat' in self._json:
lat = self._json['lat'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
if 'location' in self._json:
if 'lat' in self._json['location']:
lat = self._json['location']['lat']
# check value
if lat is None:
raise ValueError(f"Lat is None in {self._filename}")
if lat == 0:
raise ValueError(f"Lat is 0 in {self._filename}")
return lat
except KeyError:
pass
return None
def lng(self):
try:
if self.type() == PositionFile.GPS:
lng = None
# try to get value from known formats
if 'Longitude' in self._json:
lng = self._json['Longitude']
if self.type() == PositionFile.GEO:
lng = self._json['location']['lng']
if lng != 0:
return lng
raise ValueError("Lng is 0")
if 'long' in self._json:
lng = self._json['long'] # an old paw-gps format: {"long": 14.693561, "lat": 40.806375}
if 'location' in self._json:
if 'lng' in self._json['location']:
lng = self._json['location']['lng']
# check value
if lng is None:
raise ValueError(f"Lng is None in {self._filename}")
if lng == 0:
raise ValueError(f"Lng is 0 in {self._filename}")
return lng
except KeyError:
pass
return None
def accuracy(self):
if self.type() == PositionFile.GPS:
return 50.0
return 50.0 # a default
if self.type() == PositionFile.PAWGPS:
return 50.0 # a default
if self.type() == PositionFile.GEO:
try:
return self._json['accuracy']

View File

@@ -1,19 +1,27 @@
import os
import logging
import requests
from datetime import datetime
from threading import Lock
from pwnagotchi.utils import StatusFile
import pwnagotchi.plugins as plugins
from pwnagotchi import plugins
from json.decoder import JSONDecodeError
class WpaSec(plugins.Plugin):
__author__ = '33197631+dadav@users.noreply.github.com'
__version__ = '2.0.1'
__version__ = '2.1.0'
__license__ = 'GPL3'
__description__ = 'This plugin automatically uploads handshakes to https://wpa-sec.stanev.org'
def __init__(self):
self.ready = False
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
self.lock = Lock()
try:
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
except JSONDecodeError as json_err:
os.remove("/root/.wpa_sec_uploads")
self.report = StatusFile('/root/.wpa_sec_uploads', data_format='json')
self.options = dict()
self.skip = list()
@@ -35,6 +43,29 @@ class WpaSec(plugins.Plugin):
except requests.exceptions.RequestException as req_e:
raise req_e
def _download_from_wpasec(self, output, timeout=30):
"""
Downloads the results from wpasec and safes them to output
Output-Format: bssid, station_mac, ssid, password
"""
api_url = self.options['api_url']
if not api_url.endswith('/'):
api_url = f"{api_url}/"
api_url = f"{api_url}?api&dl=1"
cookie = {'key': self.options['api_key']}
try:
result = requests.get(api_url, cookies=cookie, timeout=timeout)
with open(output, 'wb') as output_file:
output_file.write(result.content)
except requests.exceptions.RequestException as req_e:
raise req_e
except OSError as os_e:
raise os_e
def on_loaded(self):
"""
Gets called when the plugin gets loaded
@@ -53,32 +84,48 @@ class WpaSec(plugins.Plugin):
"""
Called in manual mode when there's internet connectivity
"""
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
with self.lock:
if self.ready:
config = agent.config()
display = agent.view()
reported = self.report.data_field_or('reported', default=list())
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
handshake_dir = config['bettercap']['handshakes']
handshake_filenames = os.listdir(handshake_dir)
handshake_paths = [os.path.join(handshake_dir, filename) for filename in handshake_filenames if
filename.endswith('.pcap')]
handshake_new = set(handshake_paths) - set(reported) - set(self.skip)
if handshake_new:
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
if handshake_new:
logging.info("WPA_SEC: Internet connectivity detected. Uploading new handshakes to wpa-sec.stanev.org")
for idx, handshake in enumerate(handshake_new):
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_wpasec(handshake)
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("WPA_SEC: %s", req_e)
continue
except OSError as os_e:
logging.error("WPA_SEC: %s", os_e)
continue
if 'download_results' in self.options and self.options['download_results']:
cracked_file = os.path.join(handshake_dir, 'wpa-sec.cracked.potfile')
if os.path.exists(cracked_file):
last_check = datetime.fromtimestamp(os.path.getmtime(cracked_file))
if last_check is not None and ((datetime.now() - last_check).seconds / (60 * 60)) < 1:
return
for idx, handshake in enumerate(handshake_new):
display.set('status', f"Uploading handshake to wpa-sec.stanev.org ({idx + 1}/{len(handshake_new)})")
display.update(force=True)
try:
self._upload_to_wpasec(handshake)
reported.append(handshake)
self.report.update(data={'reported': reported})
logging.info("WPA_SEC: Successfully uploaded %s", handshake)
self._download_from_wpasec(os.path.join(handshake_dir, 'wpa-sec.cracked.potfile'))
logging.info("WPA_SEC: Downloaded cracked passwords.")
except requests.exceptions.RequestException as req_e:
self.skip.append(handshake)
logging.error("WPA_SEC: %s", req_e)
continue
logging.debug("WPA_SEC: %s", req_e)
except OSError as os_e:
logging.error("WPA_SEC: %s", os_e)
continue
logging.debug("WPA_SEC: %s", os_e)

View File

@@ -52,6 +52,9 @@ class Display(View):
def is_dfrobot(self):
return self._implementation.name == 'dfrobot'
def is_waveshare144lcd(self):
return self._implementation.name == 'waveshare144lcd'
def is_waveshare154inch(self):
return self._implementation.name == 'waveshare154inch'

View File

@@ -7,6 +7,7 @@ from pwnagotchi.ui.hw.waveshare1 import WaveshareV1
from pwnagotchi.ui.hw.waveshare2 import WaveshareV2
from pwnagotchi.ui.hw.waveshare27inch import Waveshare27inch
from pwnagotchi.ui.hw.waveshare29inch import Waveshare29inch
from pwnagotchi.ui.hw.waveshare144lcd import Waveshare144lcd
from pwnagotchi.ui.hw.waveshare154inch import Waveshare154inch
from pwnagotchi.ui.hw.waveshare213d import Waveshare213d
from pwnagotchi.ui.hw.spotpear24inch import Spotpear24inch
@@ -40,6 +41,9 @@ def display_for(config):
elif config['ui']['display']['type'] == 'waveshare29inch':
return Waveshare29inch(config)
elif config['ui']['display']['type'] == 'waveshare144lcd':
return Waveshare144lcd(config)
elif config['ui']['display']['type'] == 'waveshare154inch':
return Waveshare154inch(config)

View File

@@ -0,0 +1,319 @@
# -*- coding:UTF-8 -*-
##
# | file : LCD_1IN44.py
# | version : V2.0
# | date : 2018-07-16
# | function : On the ST7735S chip driver and clear screen, drawing lines, drawing, writing
# and other functions to achieve
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import RPi.GPIO as GPIO
import time
import numpy as np
from . import config
LCD_1IN44 = 1
LCD_1IN8 = 0
if LCD_1IN44 == 1:
LCD_WIDTH = 128 #LCD width
LCD_HEIGHT = 128 #LCD height
LCD_X = 2
LCD_Y = 1
if LCD_1IN8 == 1:
LCD_WIDTH = 160
LCD_HEIGHT = 128
LCD_X = 1
LCD_Y = 2
LCD_X_MAXPIXEL = 132 #LCD width maximum memory
LCD_Y_MAXPIXEL = 162 #LCD height maximum memory
#scanning method
L2R_U2D = 1
L2R_D2U = 2
R2L_U2D = 3
R2L_D2U = 4
U2D_L2R = 5
U2D_R2L = 6
D2U_L2R = 7
D2U_R2L = 8
SCAN_DIR_DFT = U2D_R2L
class LCD:
def __init__(self):
GPIO.setmode(GPIO.BCM)
self.width = LCD_WIDTH
self.height = LCD_HEIGHT
self.LCD_Scan_Dir = SCAN_DIR_DFT
self.LCD_X_Adjust = LCD_X
self.LCD_Y_Adjust = LCD_Y
""" Hardware reset """
def LCD_Reset(self):
GPIO.output(config.LCD_RST_PIN, GPIO.HIGH)
config.Driver_Delay_ms(100)
GPIO.output(config.LCD_RST_PIN, GPIO.LOW)
config.Driver_Delay_ms(100)
GPIO.output(config.LCD_RST_PIN, GPIO.HIGH)
config.Driver_Delay_ms(100)
""" Write register address and data """
def LCD_WriteReg(self, Reg):
GPIO.setmode(GPIO.BCM)
GPIO.setup(config.LCD_DC_PIN, GPIO.OUT)
GPIO.output(config.LCD_DC_PIN, GPIO.LOW)
config.SPI_Write_Byte([Reg])
def LCD_WriteData_8bit(self, Data):
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
config.SPI_Write_Byte([Data])
def LCD_WriteData_NLen16Bit(self, Data, DataLen):
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
for i in range(0, DataLen):
config.SPI_Write_Byte([Data >> 8])
config.SPI_Write_Byte([Data & 0xff])
""" Common register initialization """
def LCD_InitReg(self):
#ST7735R Frame Rate
self.LCD_WriteReg(0xB1)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
self.LCD_WriteReg(0xB2)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
self.LCD_WriteReg(0xB3)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
self.LCD_WriteData_8bit(0x01)
self.LCD_WriteData_8bit(0x2C)
self.LCD_WriteData_8bit(0x2D)
#Column inversion
self.LCD_WriteReg(0xB4)
self.LCD_WriteData_8bit(0x07)
#ST7735R Power Sequence
self.LCD_WriteReg(0xC0)
self.LCD_WriteData_8bit(0xA2)
self.LCD_WriteData_8bit(0x02)
self.LCD_WriteData_8bit(0x84)
self.LCD_WriteReg(0xC1)
self.LCD_WriteData_8bit(0xC5)
self.LCD_WriteReg(0xC2)
self.LCD_WriteData_8bit(0x0A)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteReg(0xC3)
self.LCD_WriteData_8bit(0x8A)
self.LCD_WriteData_8bit(0x2A)
self.LCD_WriteReg(0xC4)
self.LCD_WriteData_8bit(0x8A)
self.LCD_WriteData_8bit(0xEE)
self.LCD_WriteReg(0xC5)#VCOM
self.LCD_WriteData_8bit(0x0E)
#ST7735R Gamma Sequence
self.LCD_WriteReg(0xe0)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x1a)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x18)
self.LCD_WriteData_8bit(0x2f)
self.LCD_WriteData_8bit(0x28)
self.LCD_WriteData_8bit(0x20)
self.LCD_WriteData_8bit(0x22)
self.LCD_WriteData_8bit(0x1f)
self.LCD_WriteData_8bit(0x1b)
self.LCD_WriteData_8bit(0x23)
self.LCD_WriteData_8bit(0x37)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(0x07)
self.LCD_WriteData_8bit(0x02)
self.LCD_WriteData_8bit(0x10)
self.LCD_WriteReg(0xe1)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x1b)
self.LCD_WriteData_8bit(0x0f)
self.LCD_WriteData_8bit(0x17)
self.LCD_WriteData_8bit(0x33)
self.LCD_WriteData_8bit(0x2c)
self.LCD_WriteData_8bit(0x29)
self.LCD_WriteData_8bit(0x2e)
self.LCD_WriteData_8bit(0x30)
self.LCD_WriteData_8bit(0x30)
self.LCD_WriteData_8bit(0x39)
self.LCD_WriteData_8bit(0x3f)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(0x07)
self.LCD_WriteData_8bit(0x03)
self.LCD_WriteData_8bit(0x10)
#Enable test command
self.LCD_WriteReg(0xF0)
self.LCD_WriteData_8bit(0x01)
#Disable ram power save mode
self.LCD_WriteReg(0xF6)
self.LCD_WriteData_8bit(0x00)
#65k mode
self.LCD_WriteReg(0x3A)
self.LCD_WriteData_8bit(0x05)
#********************************************************************************
#function: Set the display scan and color transfer modes
#parameter:
# Scan_dir : Scan direction
# Colorchose : RGB or GBR color format
#********************************************************************************
def LCD_SetGramScanWay(self, Scan_dir):
#Get the screen scan direction
self.LCD_Scan_Dir = Scan_dir
#Get GRAM and LCD width and height
if (Scan_dir == L2R_U2D) or (Scan_dir == L2R_D2U) or (Scan_dir == R2L_U2D) or (Scan_dir == R2L_D2U) :
self.width = LCD_HEIGHT
self.height = LCD_WIDTH
if Scan_dir == L2R_U2D:
MemoryAccessReg_Data = 0X00 | 0x00
elif Scan_dir == L2R_D2U:
MemoryAccessReg_Data = 0X00 | 0x80
elif Scan_dir == R2L_U2D:
MemoryAccessReg_Data = 0x40 | 0x00
else: #R2L_D2U:
MemoryAccessReg_Data = 0x40 | 0x80
else:
self.width = LCD_WIDTH
self.height = LCD_HEIGHT
if Scan_dir == U2D_L2R:
MemoryAccessReg_Data = 0X00 | 0x00 | 0x20
elif Scan_dir == U2D_R2L:
MemoryAccessReg_Data = 0X00 | 0x40 | 0x20
elif Scan_dir == D2U_L2R:
MemoryAccessReg_Data = 0x80 | 0x00 | 0x20
else: #R2L_D2U
MemoryAccessReg_Data = 0x40 | 0x80 | 0x20
#please set (MemoryAccessReg_Data & 0x10) != 1
if (MemoryAccessReg_Data & 0x10) != 1:
self.LCD_X_Adjust = LCD_Y
self.LCD_Y_Adjust = LCD_X
else:
self.LCD_X_Adjust = LCD_X
self.LCD_Y_Adjust = LCD_Y
# Set the read / write scan direction of the frame memory
self.LCD_WriteReg(0x36) #MX, MY, RGB mode
if LCD_1IN44 == 1:
self.LCD_WriteData_8bit( MemoryAccessReg_Data | 0x08) #0x08 set RGB
else:
self.LCD_WriteData_8bit( MemoryAccessReg_Data & 0xf7) #RGB color filter panel
#/********************************************************************************
#function:
# initialization
#********************************************************************************/
def LCD_Init(self, Lcd_ScanDir):
if (config.GPIO_Init() != 0):
return -1
#Turn on the backlight
#GPIO.setup(config.LCD_BL_PIN, GPIO.OUT)
GPIO.output(config.LCD_BL_PIN,GPIO.HIGH)
#Hardware reset
self.LCD_Reset()
#Set the initialization register
self.LCD_InitReg()
#Set the display scan and color transfer modes
self.LCD_SetGramScanWay(Lcd_ScanDir)
config.Driver_Delay_ms(200)
#sleep out
self.LCD_WriteReg(0x11)
config.Driver_Delay_ms(120)
#Turn on the LCD display
self.LCD_WriteReg(0x29)
#/********************************************************************************
#function: Sets the start position and size of the display area
#parameter:
# Xstart : X direction Start coordinates
# Ystart : Y direction Start coordinates
# Xend : X direction end coordinates
# Yend : Y direction end coordinates
#********************************************************************************/
def LCD_SetWindows(self, Xstart, Ystart, Xend, Yend):
#set the X coordinates
self.LCD_WriteReg(0x2A)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit((Xstart & 0xff) + self.LCD_X_Adjust)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(((Xend - 1) & 0xff) + self.LCD_X_Adjust)
#set the Y coordinates
self.LCD_WriteReg (0x2B)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit((Ystart & 0xff) + self.LCD_Y_Adjust)
self.LCD_WriteData_8bit(0x00)
self.LCD_WriteData_8bit(((Yend - 1) & 0xff )+ self.LCD_Y_Adjust)
self.LCD_WriteReg(0x2C)
def LCD_Clear(self):
#hello
_buffer = [0xff]*(self.width * self.height * 2)
self.LCD_SetWindows(0, 0, self.width, self.height)
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
for i in range(0,len(_buffer),4096):
config.SPI_Write_Byte(_buffer[i:i+4096])
def LCD_ShowImage(self,Image,Xstart,Ystart):
if (Image == None):
return
imwidth, imheight = Image.size
if imwidth != self.width or imheight != self.height:
raise ValueError('Image must be same dimensions as display \
({0}x{1}).' .format(self.width, self.height))
img = np.asarray(Image)
pix = np.zeros((self.width,self.height,2), dtype = np.uint8)
pix[...,[0]] = np.add(np.bitwise_and(img[...,[0]],0xF8),np.right_shift(img[...,[1]],5))
pix[...,[1]] = np.add(np.bitwise_and(np.left_shift(img[...,[1]],3),0xE0),np.right_shift(img[...,[2]],3))
pix = pix.flatten().tolist()
self.LCD_SetWindows(0, 0, self.width , self.height)
GPIO.output(config.LCD_DC_PIN, GPIO.HIGH)
for i in range(0,len(pix),4096):
config.SPI_Write_Byte(pix[i:i+4096])

View File

@@ -0,0 +1,73 @@
# /*****************************************************************************
# * | File : config.py
# * | Author :
# * | Info :
# *----------------
# * | This version: V1.0
# * | Date : 2019-12-06
# * | Info :
# ******************************************************************************/
Device_SPI = 1
Device_I2C = 0
Device = Device_SPI
#spi = spidev.SpiDev(0, 0)
##
# @filename : DEV_Config.py
# @brief : LCD hardware interface implements (GPIO, SPI)
# @author : Yehui from Waveshare
#
# Copyright (C) Waveshare July 10 2017
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documnetation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
import spidev
import RPi.GPIO as GPIO
import time
# Pin definition
LCD_RST_PIN = 27
LCD_DC_PIN = 25
LCD_CS_PIN = 8
LCD_BL_PIN = 24
# SPI device, bus = 0, device = 0
SPI = spidev.SpiDev(0, 0)
def epd_digital_write(pin, value):
GPIO.output(pin, value)
def Driver_Delay_ms(xms):
time.sleep(xms / 1000.0)
def SPI_Write_Byte(data):
SPI.writebytes(data)
def GPIO_Init():
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(LCD_RST_PIN, GPIO.OUT)
GPIO.setup(LCD_DC_PIN, GPIO.OUT)
GPIO.setup(LCD_CS_PIN, GPIO.OUT)
GPIO.setup(LCD_BL_PIN, GPIO.OUT)
SPI.max_speed_hz = 9000000
SPI.mode = 0b00
return 0;
### END OF FILE ###

View File

@@ -0,0 +1,32 @@
# Waveshare 1.44inch LCD HAT
# https://www.waveshare.com/1.44inch-lcd-hat.htm
# https://www.waveshare.com/wiki/1.44inch_LCD_HAT
# Driver: ST7735S
# Interface: SPI
# Display color: RGB, 65K color
# Resolution: 128x128
# Backlight: LED
# Operating voltage: 3.3V
from . import config
from . import LCD_1in44
from PIL import ImageOps
class EPD(object):
def __init__(self):
self.width = 128
self.height = 128
self.LCD = LCD_1in44.LCD()
self.LCD.Lcd_ScanDir = LCD_1in44.SCAN_DIR_DFT #SCAN_DIR_DFT = D2U_L2R
self.LCD.LCD_Init(self.LCD.Lcd_ScanDir)
def init(self):
pass
def clear(self):
#self.LCD.LCD_Clear()
pass
def display(self, image):
rgb_im = ImageOps.colorize(image.convert("L"), black ="green", white ="black")
self.LCD.LCD_ShowImage(rgb_im, 0, 0)

View File

@@ -0,0 +1,46 @@
import logging
import pwnagotchi.ui.fonts as fonts
from pwnagotchi.ui.hw.base import DisplayImpl
class Waveshare144lcd(DisplayImpl):
def __init__(self, config):
super(Waveshare144lcd, self).__init__(config, 'waveshare144lcd')
self._display = None
def layout(self):
fonts.setup(10, 8, 10, 18)
self._layout['width'] = 128
self._layout['height'] = 128
self._layout['face'] = (0, 43)
self._layout['name'] = (0, 14)
self._layout['channel'] = (0, 0)
self._layout['aps'] = (0, 71)
self._layout['uptime'] = (0, 25)
self._layout['line1'] = [0, 12, 127, 12]
self._layout['line2'] = [0, 116, 127, 116]
self._layout['friend_face'] = (12, 88)
self._layout['friend_name'] = (1, 103)
self._layout['shakes'] = (26, 117)
self._layout['mode'] = (0, 117)
self._layout['status'] = {
'pos': (65, 26),
'font': fonts.Small,
'max': 12
}
return self._layout
def initialize(self):
logging.info("initializing waveshare 1.44 inch lcd display")
from pwnagotchi.ui.hw.libs.waveshare.lcdhat144.epd import EPD
self._display = EPD()
self._display.init()
self._display.clear()
def render(self, canvas):
self._display.display(canvas)
def clear(self):
pass
#self._display.clear()

View File

@@ -34,6 +34,7 @@ class Handler:
self._app.add_url_rule('/ui', 'ui', self.with_auth(self.ui))
self._app.add_url_rule('/shutdown', 'shutdown', self.with_auth(self.shutdown), methods=['POST'])
self._app.add_url_rule('/reboot', 'reboot', self.with_auth(self.reboot), methods=['POST'])
self._app.add_url_rule('/restart', 'restart', self.with_auth(self.restart), methods=['POST'])
# inbox
@@ -179,16 +180,19 @@ class Handler:
def plugins(self, name, subpath):
if name is None:
# show plugins overview
abort(404)
return render_template('plugins.html', loaded=plugins.loaded, database=plugins.database)
if name == 'toggle' and request.method == 'POST':
checked = True if 'enabled' in request.form else False
return 'success' if plugins.toggle_plugin(request.form['plugin'], checked) else 'failed'
if name in plugins.loaded and plugins.loaded[name] is not None and hasattr(plugins.loaded[name], 'on_webhook'):
try:
return plugins.loaded[name].on_webhook(subpath, request)
except Exception:
abort(500)
else:
if name in plugins.loaded and hasattr(plugins.loaded[name], 'on_webhook'):
try:
return plugins.loaded[name].on_webhook(subpath, request)
except Exception:
abort(500)
else:
abort(404)
abort(404)
# serve a message and shuts down the unit
def shutdown(self):
@@ -198,6 +202,14 @@ class Handler:
finally:
_thread.start_new_thread(pwnagotchi.shutdown, ())
# serve a message and reboot the unit
def reboot(self):
try:
return render_template('status.html', title=pwnagotchi.name(), go_back_after=60,
message='Rebooting ...')
finally:
_thread.start_new_thread(pwnagotchi.reboot, ())
# serve a message and restart the unit in the other mode
def restart(self):
mode = request.form['mode']

View File

@@ -0,0 +1,259 @@
/*rules for the plot target div. These will be cascaded down to all plot elements according to css rules*/
.jqplot-target {
position: relative;
color: #666666;
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 1em;
/* height: 300px;
width: 400px;*/
}
/*rules applied to all axes*/
.jqplot-axis {
font-size: 0.75em;
}
.jqplot-xaxis {
margin-top: 10px;
}
.jqplot-x2axis {
margin-bottom: 10px;
}
.jqplot-yaxis {
margin-right: 10px;
}
.jqplot-y2axis, .jqplot-y3axis, .jqplot-y4axis, .jqplot-y5axis, .jqplot-y6axis, .jqplot-y7axis, .jqplot-y8axis, .jqplot-y9axis, .jqplot-yMidAxis {
margin-left: 10px;
margin-right: 10px;
}
/*rules applied to all axis tick divs*/
.jqplot-axis-tick, .jqplot-xaxis-tick, .jqplot-yaxis-tick, .jqplot-x2axis-tick, .jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick, .jqplot-yMidAxis-tick {
position: absolute;
white-space: pre;
}
.jqplot-xaxis-tick {
top: 0px;
/* initial position untill tick is drawn in proper place */
left: 15px;
/* padding-top: 10px;*/
vertical-align: top;
}
.jqplot-x2axis-tick {
bottom: 0px;
/* initial position untill tick is drawn in proper place */
left: 15px;
/* padding-bottom: 10px;*/
vertical-align: bottom;
}
.jqplot-yaxis-tick {
right: 0px;
/* initial position untill tick is drawn in proper place */
top: 15px;
/* padding-right: 10px;*/
text-align: right;
}
.jqplot-yaxis-tick.jqplot-breakTick {
right: -20px;
margin-right: 0px;
padding:1px 5px 1px 5px;
/*background-color: white;*/
z-index: 2;
font-size: 1.5em;
}
.jqplot-y2axis-tick, .jqplot-y3axis-tick, .jqplot-y4axis-tick, .jqplot-y5axis-tick, .jqplot-y6axis-tick, .jqplot-y7axis-tick, .jqplot-y8axis-tick, .jqplot-y9axis-tick {
left: 0px;
/* initial position untill tick is drawn in proper place */
top: 15px;
/* padding-left: 10px;*/
/* padding-right: 15px;*/
text-align: left;
}
.jqplot-yMidAxis-tick {
text-align: center;
white-space: nowrap;
}
.jqplot-xaxis-label {
margin-top: 10px;
font-size: 11pt;
position: absolute;
}
.jqplot-x2axis-label {
margin-bottom: 10px;
font-size: 11pt;
position: absolute;
}
.jqplot-yaxis-label {
margin-right: 10px;
/* text-align: center;*/
font-size: 11pt;
position: absolute;
}
.jqplot-yMidAxis-label {
font-size: 11pt;
position: absolute;
}
.jqplot-y2axis-label, .jqplot-y3axis-label, .jqplot-y4axis-label, .jqplot-y5axis-label, .jqplot-y6axis-label, .jqplot-y7axis-label, .jqplot-y8axis-label, .jqplot-y9axis-label {
/* text-align: center;*/
font-size: 11pt;
margin-left: 10px;
position: absolute;
}
.jqplot-meterGauge-tick {
font-size: 0.75em;
color: #999999;
}
.jqplot-meterGauge-label {
font-size: 1em;
color: #999999;
}
table.jqplot-table-legend {
margin-top: 12px;
margin-bottom: 12px;
margin-left: 12px;
margin-right: 12px;
}
table.jqplot-table-legend, table.jqplot-cursor-legend {
background-color: rgba(255,255,255,0.6);
border: 1px solid #cccccc;
position: absolute;
font-size: 0.75em;
}
td.jqplot-table-legend {
vertical-align:middle;
}
/*
These rules could be used instead of assigning
element styles and relying on js object properties.
*/
/*
td.jqplot-table-legend-swatch {
padding-top: 0.5em;
text-align: center;
}
tr.jqplot-table-legend:first td.jqplot-table-legend-swatch {
padding-top: 0px;
}
*/
td.jqplot-seriesToggle:hover, td.jqplot-seriesToggle:active {
cursor: pointer;
}
.jqplot-table-legend .jqplot-series-hidden {
text-decoration: line-through;
}
div.jqplot-table-legend-swatch-outline {
border: 1px solid #cccccc;
padding:1px;
}
div.jqplot-table-legend-swatch {
width:0px;
height:0px;
border-top-width: 5px;
border-bottom-width: 5px;
border-left-width: 6px;
border-right-width: 6px;
border-top-style: solid;
border-bottom-style: solid;
border-left-style: solid;
border-right-style: solid;
}
.jqplot-title {
top: 0px;
left: 0px;
padding-bottom: 0.5em;
font-size: 1.2em;
}
table.jqplot-cursor-tooltip {
border: 1px solid #cccccc;
font-size: 0.75em;
}
.jqplot-cursor-tooltip {
border: 1px solid #cccccc;
font-size: 0.75em;
white-space: nowrap;
background: rgba(208,208,208,0.5);
padding: 1px;
}
.jqplot-highlighter-tooltip, .jqplot-canvasOverlay-tooltip {
border: 1px solid #cccccc;
font-size: 0.75em;
white-space: nowrap;
background: rgba(208,208,208,0.5);
padding: 1px;
}
.jqplot-point-label {
font-size: 0.75em;
z-index: 2;
}
td.jqplot-cursor-legend-swatch {
vertical-align: middle;
text-align: center;
}
div.jqplot-cursor-legend-swatch {
width: 1.2em;
height: 0.7em;
}
.jqplot-error {
/* Styles added to the plot target container when there is an error go here.*/
text-align: center;
}
.jqplot-error-message {
/* Styling of the custom error message div goes here.*/
position: relative;
top: 46%;
display: inline-block;
}
div.jqplot-bubble-label {
font-size: 0.8em;
/* background: rgba(90%, 90%, 90%, 0.15);*/
padding-left: 2px;
padding-right: 2px;
color: rgb(20%, 20%, 20%);
}
div.jqplot-bubble-label.jqplot-bubble-label-highlight {
background: rgba(90%, 90%, 90%, 0.7);
}
div.jqplot-noData-container {
text-align: center;
background-color: rgba(96%, 96%, 96%, 0.3);
}

View File

@@ -0,0 +1 @@
.jqplot-xaxis,.jqplot-xaxis-label{margin-top:10px}.jqplot-x2axis,.jqplot-x2axis-label{margin-bottom:10px}.jqplot-target{position:relative;color:#666;font-family:"Trebuchet MS",Arial,Helvetica,sans-serif;font-size:1em}.jqplot-axis{font-size:.75em}.jqplot-yaxis{margin-right:10px}.jqplot-y2axis,.jqplot-y3axis,.jqplot-y4axis,.jqplot-y5axis,.jqplot-y6axis,.jqplot-y7axis,.jqplot-y8axis,.jqplot-y9axis,.jqplot-yMidAxis{margin-left:10px;margin-right:10px}.jqplot-axis-tick,.jqplot-x2axis-tick,.jqplot-xaxis-tick,.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick,.jqplot-yMidAxis-tick,.jqplot-yaxis-tick{position:absolute;white-space:pre}.jqplot-xaxis-tick{top:0;left:15px;vertical-align:top}.jqplot-x2axis-tick{bottom:0;left:15px;vertical-align:bottom}.jqplot-yaxis-tick{right:0;top:15px;text-align:right}.jqplot-yaxis-tick.jqplot-breakTick{right:-20px;margin-right:0;padding:1px 5px;z-index:2;font-size:1.5em}.jqplot-x2axis-label,.jqplot-xaxis-label,.jqplot-yMidAxis-label,.jqplot-yaxis-label{font-size:11pt;position:absolute}.jqplot-y2axis-tick,.jqplot-y3axis-tick,.jqplot-y4axis-tick,.jqplot-y5axis-tick,.jqplot-y6axis-tick,.jqplot-y7axis-tick,.jqplot-y8axis-tick,.jqplot-y9axis-tick{left:0;top:15px;text-align:left}.jqplot-yMidAxis-tick{text-align:center;white-space:nowrap}.jqplot-yaxis-label{margin-right:10px}.jqplot-y2axis-label,.jqplot-y3axis-label,.jqplot-y4axis-label,.jqplot-y5axis-label,.jqplot-y6axis-label,.jqplot-y7axis-label,.jqplot-y8axis-label,.jqplot-y9axis-label{font-size:11pt;margin-left:10px;position:absolute}.jqplot-meterGauge-tick{font-size:.75em;color:#999}.jqplot-meterGauge-label{font-size:1em;color:#999}table.jqplot-table-legend{margin:12px}table.jqplot-cursor-legend,table.jqplot-table-legend{background-color:rgba(255,255,255,.6);border:1px solid #ccc;position:absolute;font-size:.75em}td.jqplot-table-legend{vertical-align:middle}td.jqplot-seriesToggle:active,td.jqplot-seriesToggle:hover{cursor:pointer}.jqplot-table-legend .jqplot-series-hidden{text-decoration:line-through}div.jqplot-table-legend-swatch-outline{border:1px solid #ccc;padding:1px}div.jqplot-table-legend-swatch{width:0;height:0;border-width:5px 6px;border-style:solid}.jqplot-title{top:0;left:0;padding-bottom:.5em;font-size:1.2em}table.jqplot-cursor-tooltip{border:1px solid #ccc;font-size:.75em}.jqplot-canvasOverlay-tooltip,.jqplot-cursor-tooltip,.jqplot-highlighter-tooltip{border:1px solid #ccc;font-size:.75em;white-space:nowrap;background:rgba(208,208,208,.5);padding:1px}.jqplot-point-label{font-size:.75em;z-index:2}td.jqplot-cursor-legend-swatch{vertical-align:middle;text-align:center}div.jqplot-cursor-legend-swatch{width:1.2em;height:.7em}.jqplot-error{text-align:center}.jqplot-error-message{position:relative;top:46%;display:inline-block}div.jqplot-bubble-label{font-size:.8em;padding-left:2px;padding-right:2px;color:rgb(20%,20%,20%)}div.jqplot-bubble-label.jqplot-bubble-label-highlight{background:rgba(90%,90%,90%,.7)}div.jqplot-noData-container{text-align:center;background-color:rgba(96%,96%,96%,.3)}

View File

@@ -31,4 +31,52 @@ a.read {
p.messagebody {
padding: 1em;
}
}
li.navitem {
width: 16.66% !important;
clear: none !important;
}
/* Custom indentations are needed because the length of custom labels differs from
the length of the standard labels */
.custom-size-flipswitch.ui-flipswitch .ui-btn.ui-flipswitch-on {
text-indent: -5.9em;
}
.custom-size-flipswitch.ui-flipswitch .ui-flipswitch-off {
text-indent: 0.5em;
}
/* Custom widths are needed because the length of custom labels differs from
the length of the standard labels */
.custom-size-flipswitch.ui-flipswitch {
width: 8.875em;
}
.custom-size-flipswitch.ui-flipswitch.ui-flipswitch-active {
padding-left: 7em;
width: 1.875em;
}
@media (min-width: 28em) {
/*Repeated from rule .ui-flipswitch above*/
.ui-field-contain > label + .custom-size-flipswitch.ui-flipswitch {
width: 1.875em;
}
}
#container {
display: flex;
flex-wrap: wrap;
justify-content: space-around;
}
.plugins-box {
margin: 0.5rem;
padding: 0.2rem;
border-style: groove;
border-radius: 0.5rem;
background-color: lightgrey;
text-align: center;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,314 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// Class: $.jqplot.BezierCurveRenderer.js
// Renderer which draws lines as stacked bezier curves.
// Data for the line will not be specified as an array of
// [x, y] data point values, but as a an array of [start piont, bezier curve]
// So, the line is specified as: [[xstart, ystart], [cp1x, cp1y, cp2x, cp2y, xend, yend]].
$.jqplot.BezierCurveRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.BezierCurveRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.BezierCurveRenderer.prototype.constructor = $.jqplot.BezierCurveRenderer;
// Method: setGridData
// converts the user data values to grid coordinates and stores them
// in the gridData array.
// Called with scope of a series.
$.jqplot.BezierCurveRenderer.prototype.setGridData = function(plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
// this._plotData should be same as this.data
var data = this.data;
this.gridData = [];
this._prevGridData = [];
// if seriesIndex = 0, fill to x axis.
// if seriesIndex > 0, fill to previous series data.
var idx = this.index;
if (data.length == 2) {
if (idx == 0) {
this.gridData = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
[xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, this._yaxis.min)],
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
];
}
else {
var psd = plot.series[idx-1].data;
this.gridData = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
[xp.call(this._xaxis, psd[1][4]), yp.call(this._yaxis, psd[1][5])],
[xp.call(this._xaxis, psd[1][2]), yp.call(this._yaxis, psd[1][3]),
xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
];
}
}
else {
if (idx == 0) {
this.gridData = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
[xp.call(this._xaxis, data[3][1]), yp.call(this._yaxis, this._yaxis.min)],
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
];
}
else {
var psd = plot.series[idx-1].data;
this.gridData = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
[xp.call(this._xaxis, psd[3][0]), yp.call(this._yaxis, psd[3][1])],
[xp.call(this._xaxis, psd[2][0]), yp.call(this._yaxis, psd[2][1]),
xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
];
}
}
};
// Method: makeGridData
// converts any arbitrary data values to grid coordinates and
// returns them. This method exists so that plugins can use a series'
// linerenderer to generate grid data points without overwriting the
// grid data associated with that series.
// Called with scope of a series.
$.jqplot.BezierCurveRenderer.prototype.makeGridData = function(data, plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var gd = [];
var pgd = [];
// if seriesIndex = 0, fill to x axis.
// if seriesIndex > 0, fill to previous series data.
var idx = this.index;
if (data.length == 2) {
if (idx == 0) {
gd = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
[xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, this._yaxis.min)],
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
];
}
else {
var psd = plot.series[idx-1].data;
gd = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[1][2]), yp.call(this._yaxis, data[1][3]),
xp.call(this._xaxis, data[1][4]), yp.call(this._yaxis, data[1][5])],
[xp.call(this._xaxis, psd[1][4]), yp.call(this._yaxis, psd[1][5])],
[xp.call(this._xaxis, psd[1][2]), yp.call(this._yaxis, psd[1][3]),
xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
];
}
}
else {
if (idx == 0) {
gd = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
[xp.call(this._xaxis, data[3][1]), yp.call(this._yaxis, this._yaxis.min)],
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, this._yaxis.min)]
];
}
else {
var psd = plot.series[idx-1].data;
gd = [
[xp.call(this._xaxis, data[0][0]), yp.call(this._yaxis, data[0][1])],
[xp.call(this._xaxis, data[1][0]), yp.call(this._yaxis, data[1][1]),
xp.call(this._xaxis, data[2][0]), yp.call(this._yaxis, data[2][1]),
xp.call(this._xaxis, data[3][0]), yp.call(this._yaxis, data[3][1])],
[xp.call(this._xaxis, psd[3][0]), yp.call(this._yaxis, psd[3][1])],
[xp.call(this._xaxis, psd[2][0]), yp.call(this._yaxis, psd[2][1]),
xp.call(this._xaxis, psd[1][0]), yp.call(this._yaxis, psd[1][1]),
xp.call(this._xaxis, psd[0][0]), yp.call(this._yaxis, psd[0][1])]
];
}
}
return gd;
};
// called within scope of series.
$.jqplot.BezierCurveRenderer.prototype.draw = function(ctx, gd, options) {
var i;
ctx.save();
if (gd.length) {
if (this.showLine) {
ctx.save();
var opts = (options != null) ? options : {};
ctx.fillStyle = opts.fillStyle || this.color;
ctx.beginPath();
ctx.moveTo(gd[0][0], gd[0][1]);
ctx.bezierCurveTo(gd[1][0], gd[1][1], gd[1][2], gd[1][3], gd[1][4], gd[1][5]);
ctx.lineTo(gd[2][0], gd[2][1]);
if (gd[3].length == 2) {
ctx.lineTo(gd[3][0], gd[3][1]);
}
else {
ctx.bezierCurveTo(gd[3][0], gd[3][1], gd[3][2], gd[3][3], gd[3][4], gd[3][5]);
}
ctx.closePath();
ctx.fill();
ctx.restore();
}
}
ctx.restore();
};
$.jqplot.BezierCurveRenderer.prototype.drawShadow = function(ctx, gd, options) {
// This is a no-op, shadows drawn with lines.
};
$.jqplot.BezierAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
};
$.jqplot.BezierAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.BezierAxisRenderer.prototype.constructor = $.jqplot.BezierAxisRenderer;
// Axes on a plot with Bezier Curves
$.jqplot.BezierAxisRenderer.prototype.init = function(options){
$.extend(true, this, options);
var db = this._dataBounds;
// Go through all the series attached to this axis and find
// the min/max bounds for this axis.
for (var i=0; i<this._series.length; i++) {
var s = this._series[i];
var d = s.data;
if (d.length == 4) {
for (var j=0; j<d.length; j++) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
if (d[j][0] < db.min || db.min == null) {
db.min = d[j][0];
}
if (d[j][0] > db.max || db.max == null) {
db.max = d[j][0];
}
}
else {
if (d[j][1] < db.min || db.min == null) {
db.min = d[j][1];
}
if (d[j][1] > db.max || db.max == null) {
db.max = d[j][1];
}
}
}
}
else {
if (this.name == 'xaxis' || this.name == 'x2axis') {
if (d[0][0] < db.min || db.min == null) {
db.min = d[0][0];
}
if (d[0][0] > db.max || db.max == null) {
db.max = d[0][0];
}
for (var j=0; j<5; j+=2) {
if (d[1][j] < db.min || db.min == null) {
db.min = d[1][j];
}
if (d[1][j] > db.max || db.max == null) {
db.max = d[1][j];
}
}
}
else {
if (d[0][1] < db.min || db.min == null) {
db.min = d[0][1];
}
if (d[0][1] > db.max || db.max == null) {
db.max = d[0][1];
}
for (var j=1; j<6; j+=2) {
if (d[1][j] < db.min || db.min == null) {
db.min = d[1][j];
}
if (d[1][j] > db.max || db.max == null) {
db.max = d[1][j];
}
}
}
}
}
};
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = $.extend(true, {pad:0}, options.axesDefaults);
options.seriesDefaults = options.seriesDefaults || {};
options.legend = $.extend(true, {placement:'outside'}, options.legend);
// only set these if there is a pie series
var setopts = false;
if (options.seriesDefaults.renderer == $.jqplot.BezierCurveRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer == $.jqplot.BezierCurveRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.BezierAxisRenderer;
}
}
$.jqplot.preInitHooks.push(preInit);
})(jQuery);

View File

@@ -0,0 +1,801 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// Class: $.jqplot.BarRenderer
// A plugin renderer for jqPlot to draw a bar plot.
// Draws series as a line.
$.jqplot.BarRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.BarRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.BarRenderer.prototype.constructor = $.jqplot.BarRenderer;
// called with scope of series.
$.jqplot.BarRenderer.prototype.init = function(options, plot) {
// Group: Properties
//
// prop: barPadding
// Number of pixels between adjacent bars at the same axis value.
this.barPadding = 8;
// prop: barMargin
// Number of pixels between groups of bars at adjacent axis values.
this.barMargin = 10;
// prop: barDirection
// 'vertical' = up and down bars, 'horizontal' = side to side bars
this.barDirection = 'vertical';
// prop: barWidth
// Width of the bar in pixels (auto by devaul). null = calculated automatically.
this.barWidth = null;
// prop: shadowOffset
// offset of the shadow from the slice and offset of
// each succesive stroke of the shadow from the last.
this.shadowOffset = 2;
// prop: shadowDepth
// number of strokes to apply to the shadow,
// each stroke offset shadowOffset from the last.
this.shadowDepth = 5;
// prop: shadowAlpha
// transparency of the shadow (0 = transparent, 1 = opaque)
this.shadowAlpha = 0.08;
// prop: waterfall
// true to enable waterfall plot.
this.waterfall = false;
// prop: groups
// group bars into this many groups
this.groups = 1;
// prop: varyBarColor
// true to color each bar of a series separately rather than
// have every bar of a given series the same color.
// If used for non-stacked multiple series bar plots, user should
// specify a separate 'seriesColors' array for each series.
// Otherwise, each series will set their bars to the same color array.
// This option has no Effect for stacked bar charts and is disabled.
this.varyBarColor = false;
// prop: highlightMouseOver
// True to highlight slice when moused over.
// This must be false to enable highlightMouseDown to highlight when clicking on a slice.
this.highlightMouseOver = true;
// prop: highlightMouseDown
// True to highlight when a mouse button is pressed over a slice.
// This will be disabled if highlightMouseOver is true.
this.highlightMouseDown = false;
// prop: highlightColors
// an array of colors to use when highlighting a bar.
this.highlightColors = [];
// prop: transposedData
// NOT IMPLEMENTED YET. True if this is a horizontal bar plot and
// x and y values are "transposed". Tranposed, or "swapped", data is
// required prior to rev. 894 builds of jqPlot with horizontal bars.
// Allows backward compatability of bar renderer horizontal bars with
// old style data sets.
this.transposedData = true;
this.renderer.animation = {
show: false,
direction: 'down',
speed: 3000,
_supported: true
};
this._type = 'bar';
// if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
if (options.highlightMouseDown && options.highlightMouseOver == null) {
options.highlightMouseOver = false;
}
//////
// This is probably wrong here.
// After going back and forth on whether renderer should be the thing
// or extend the thing, it seems that it it best if it is a property
// on the thing. This should be something that is commonized
// among series renderers in the future.
//////
$.extend(true, this, options);
// really should probably do this
$.extend(true, this.renderer, options);
// fill is still needed to properly draw the legend.
// bars have to be filled.
this.fill = true;
// if horizontal bar and animating, reset the default direction
if (this.barDirection === 'horizontal' && this.rendererOptions.animation && this.rendererOptions.animation.direction == null) {
this.renderer.animation.direction = 'left';
}
if (this.waterfall) {
this.fillToZero = false;
this.disableStack = true;
}
if (this.barDirection == 'vertical' ) {
this._primaryAxis = '_xaxis';
this._stackAxis = 'y';
this.fillAxis = 'y';
}
else {
this._primaryAxis = '_yaxis';
this._stackAxis = 'x';
this.fillAxis = 'x';
}
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// total number of values for all bar series, total number of bar series, and position of this series
this._plotSeriesInfo = null;
// Array of actual data colors used for each data point.
this._dataColors = [];
this._barPoints = [];
// set the shape renderer options
var opts = {lineJoin:'miter', lineCap:'round', fill:true, isarc:false, strokeStyle:this.color, fillStyle:this.color, closePath:this.fill};
this.renderer.shapeRenderer.init(opts);
// set the shadow renderer options
var sopts = {lineJoin:'miter', lineCap:'round', fill:true, isarc:false, angle:this.shadowAngle, offset:this.shadowOffset, alpha:this.shadowAlpha, depth:this.shadowDepth, closePath:this.fill};
this.renderer.shadowRenderer.init(sopts);
plot.postInitHooks.addOnce(postInit);
plot.postDrawHooks.addOnce(postPlotDraw);
plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
};
// called with scope of series
function barPreInit(target, data, seriesDefaults, options) {
if (this.rendererOptions.barDirection == 'horizontal') {
this._stackAxis = 'x';
this._primaryAxis = '_yaxis';
}
if (this.rendererOptions.waterfall == true) {
this._data = $.extend(true, [], this.data);
var sum = 0;
var pos = (!this.rendererOptions.barDirection || this.rendererOptions.barDirection === 'vertical' || this.transposedData === false) ? 1 : 0;
for(var i=0; i<this.data.length; i++) {
sum += this.data[i][pos];
if (i>0) {
this.data[i][pos] += this.data[i-1][pos];
}
}
this.data[this.data.length] = (pos == 1) ? [this.data.length+1, sum] : [sum, this.data.length+1];
this._data[this._data.length] = (pos == 1) ? [this._data.length+1, sum] : [sum, this._data.length+1];
}
if (this.rendererOptions.groups > 1) {
this.breakOnNull = true;
var l = this.data.length;
var skip = parseInt(l/this.rendererOptions.groups, 10);
var count = 0;
for (var i=skip; i<l; i+=skip) {
this.data.splice(i+count, 0, [null, null]);
this._plotData.splice(i+count, 0, [null, null]);
this._stackData.splice(i+count, 0, [null, null]);
count++;
}
for (i=0; i<this.data.length; i++) {
if (this._primaryAxis == '_xaxis') {
this.data[i][0] = i+1;
this._plotData[i][0] = i+1;
this._stackData[i][0] = i+1;
}
else {
this.data[i][1] = i+1;
this._plotData[i][1] = i+1;
this._stackData[i][1] = i+1;
}
}
}
}
$.jqplot.preSeriesInitHooks.push(barPreInit);
// needs to be called with scope of series, not renderer.
$.jqplot.BarRenderer.prototype.calcSeriesNumbers = function() {
var nvals = 0;
var nseries = 0;
var paxis = this[this._primaryAxis];
var s, series, pos;
// loop through all series on this axis
for (var i=0; i < paxis._series.length; i++) {
series = paxis._series[i];
if (series === this) {
pos = i;
}
// is the series rendered as a bar?
if (series.renderer.constructor == $.jqplot.BarRenderer) {
// gridData may not be computed yet, use data length insted
nvals += series.data.length;
nseries += 1;
}
}
// return total number of values for all bar series, total number of bar series, and position of this series
return [nvals, nseries, pos];
};
$.jqplot.BarRenderer.prototype.setBarWidth = function() {
// need to know how many data values we have on the approprate axis and figure it out.
var i;
var nvals = 0;
var nseries = 0;
var paxis = this[this._primaryAxis];
var s, series, pos;
var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
nvals = temp[0];
nseries = temp[1];
var nticks = paxis.numberTicks;
var nbins = (nticks-1)/2;
// so, now we have total number of axis values.
if (paxis.name == 'xaxis' || paxis.name == 'x2axis') {
if (this._stack) {
this.barWidth = (paxis._offsets.max - paxis._offsets.min) / nvals * nseries - this.barMargin;
}
else {
this.barWidth = ((paxis._offsets.max - paxis._offsets.min)/nbins - this.barPadding * (nseries-1) - this.barMargin*2)/nseries;
// this.barWidth = (paxis._offsets.max - paxis._offsets.min) / nvals - this.barPadding - this.barMargin/nseries;
}
}
else {
if (this._stack) {
this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals * nseries - this.barMargin;
}
else {
this.barWidth = ((paxis._offsets.min - paxis._offsets.max)/nbins - this.barPadding * (nseries-1) - this.barMargin*2)/nseries;
// this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals - this.barPadding - this.barMargin/nseries;
}
}
return [nvals, nseries];
};
function computeHighlightColors (colors) {
var ret = [];
for (var i=0; i<colors.length; i++){
var rgba = $.jqplot.getColorComponents(colors[i]);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var sum = newrgb[0] + newrgb[1] + newrgb[2];
for (var j=0; j<3; j++) {
// when darkening, lowest color component can be is 60.
newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
newrgb[j] = parseInt(newrgb[j], 10);
}
ret.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
}
return ret;
}
function getStart(sidx, didx, comp, plot, axis) {
// check if sign change
var seriesIndex = sidx,
prevSeriesIndex = sidx - 1,
start,
prevVal,
aidx = (axis === 'x') ? 0 : 1;
// is this not the first series?
if (seriesIndex > 0) {
prevVal = plot.series[prevSeriesIndex]._plotData[didx][aidx];
// is there a sign change
if ((comp * prevVal) < 0) {
start = getStart(prevSeriesIndex, didx, comp, plot, axis);
}
// no sign change.
else {
start = plot.series[prevSeriesIndex].gridData[didx][aidx];
}
}
// if first series, return value at 0
else {
start = (aidx === 0) ? plot.series[seriesIndex]._xaxis.series_u2p(0) : plot.series[seriesIndex]._yaxis.series_u2p(0);
}
return start;
}
$.jqplot.BarRenderer.prototype.draw = function(ctx, gridData, options, plot) {
var i;
// Ughhh, have to make a copy of options b/c it may be modified later.
var opts = $.extend({}, options);
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
var xaxis = this.xaxis;
var yaxis = this.yaxis;
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var pointx, pointy;
// clear out data colors.
this._dataColors = [];
this._barPoints = [];
if (this.barWidth == null || this.rendererOptions.barWidth == null) {//check pull request https://bitbucket.org/cleonello/jqplot/pull-request/61/fix-for-issue-513/diff
this.renderer.setBarWidth.call(this);
}
var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
var nvals = temp[0];
var nseries = temp[1];
var pos = temp[2];
var points = [];
if (this._stack) {
this._barNudge = 0;
}
else {
this._barNudge = (-Math.abs(nseries/2 - 0.5) + pos) * (this.barWidth + this.barPadding);
}
if (showLine) {
var negativeColors = new $.jqplot.ColorGenerator(this.negativeSeriesColors);
var positiveColors = new $.jqplot.ColorGenerator(this.seriesColors);
var negativeColor = negativeColors.get(this.index);
if (! this.useNegativeColors) {
negativeColor = opts.fillStyle;
}
var positiveColor = opts.fillStyle;
var base;
var xstart;
var ystart;
if (this.barDirection == 'vertical') {
for (var i=0; i<gridData.length; i++) {
if (!this._stack && this.data[i][1] == null) {
continue;
}
points = [];
base = gridData[i][0] + this._barNudge;
// stacked
if (this._stack && this._prevGridData.length) {
ystart = getStart(this.index, i, this._plotData[i][1], plot, 'y');
}
// not stacked
else {
if (this.fillToZero) {
ystart = this._yaxis.series_u2p(0);
}
else if (this.waterfall && i > 0 && i < this.gridData.length-1) {
ystart = this.gridData[i-1][1];
}
else if (this.waterfall && i == 0 && i < this.gridData.length-1) {
if (this._yaxis.min <= 0 && this._yaxis.max >= 0) {
ystart = this._yaxis.series_u2p(0);
}
else if (this._yaxis.min > 0) {
ystart = ctx.canvas.height;
}
else {
ystart = 0;
}
}
else if (this.waterfall && i == this.gridData.length - 1) {
if (this._yaxis.min <= 0 && this._yaxis.max >= 0) {
ystart = this._yaxis.series_u2p(0);
}
else if (this._yaxis.min > 0) {
ystart = ctx.canvas.height;
}
else {
ystart = 0;
}
}
else {
ystart = ctx.canvas.height;
}
}
if ((this.fillToZero && this._plotData[i][1] < 0) || (this.waterfall && this._data[i][1] < 0)) {
if (this.varyBarColor && !this._stack) {
if (this.useNegativeColors) {
opts.fillStyle = negativeColors.next();
}
else {
opts.fillStyle = positiveColors.next();
}
}
else {
opts.fillStyle = negativeColor;
}
}
else {
if (this.varyBarColor && !this._stack) {
opts.fillStyle = positiveColors.next();
}
else {
opts.fillStyle = positiveColor;
}
}
if (!this.fillToZero || this._plotData[i][1] >= 0) {
points.push([base-this.barWidth/2, ystart]);
points.push([base-this.barWidth/2, gridData[i][1]]);
points.push([base+this.barWidth/2, gridData[i][1]]);
points.push([base+this.barWidth/2, ystart]);
}
// for negative bars make sure points are always ordered clockwise
else {
points.push([base-this.barWidth/2, gridData[i][1]]);
points.push([base-this.barWidth/2, ystart]);
points.push([base+this.barWidth/2, ystart]);
points.push([base+this.barWidth/2, gridData[i][1]]);
}
this._barPoints.push(points);
// now draw the shadows if not stacked.
// for stacked plots, they are predrawn by drawShadow
if (shadow && !this._stack) {
var sopts = $.extend(true, {}, opts);
// need to get rid of fillStyle on shadow.
delete sopts.fillStyle;
this.renderer.shadowRenderer.draw(ctx, points, sopts);
}
var clr = opts.fillStyle || this.color;
this._dataColors.push(clr);
this.renderer.shapeRenderer.draw(ctx, points, opts);
}
}
else if (this.barDirection == 'horizontal'){
for (var i=0; i<gridData.length; i++) {
if (!this._stack && this.data[i][0] == null) {
continue;
}
points = [];
base = gridData[i][1] - this._barNudge;
xstart;
if (this._stack && this._prevGridData.length) {
xstart = getStart(this.index, i, this._plotData[i][0], plot, 'x');
}
// not stacked
else {
if (this.fillToZero) {
xstart = this._xaxis.series_u2p(0);
}
else if (this.waterfall && i > 0 && i < this.gridData.length-1) {
xstart = this.gridData[i-1][0];
}
else if (this.waterfall && i == 0 && i < this.gridData.length-1) {
if (this._xaxis.min <= 0 && this._xaxis.max >= 0) {
xstart = this._xaxis.series_u2p(0);
}
else if (this._xaxis.min > 0) {
xstart = 0;
}
else {
xstart = 0;
}
}
else if (this.waterfall && i == this.gridData.length - 1) {
if (this._xaxis.min <= 0 && this._xaxis.max >= 0) {
xstart = this._xaxis.series_u2p(0);
}
else if (this._xaxis.min > 0) {
xstart = 0;
}
else {
xstart = ctx.canvas.width;
}
}
else {
xstart = 0;
}
}
if ((this.fillToZero && this._plotData[i][0] < 0) || (this.waterfall && this._data[i][0] < 0)) {
if (this.varyBarColor && !this._stack) {
if (this.useNegativeColors) {
opts.fillStyle = negativeColors.next();
}
else {
opts.fillStyle = positiveColors.next();
}
}
else {
opts.fillStyle = negativeColor;
}
}
else {
if (this.varyBarColor && !this._stack) {
opts.fillStyle = positiveColors.next();
}
else {
opts.fillStyle = positiveColor;
}
}
if (!this.fillToZero || this._plotData[i][0] >= 0) {
points.push([xstart, base + this.barWidth / 2]);
points.push([xstart, base - this.barWidth / 2]);
points.push([gridData[i][0], base - this.barWidth / 2]);
points.push([gridData[i][0], base + this.barWidth / 2]);
}
else {
points.push([gridData[i][0], base + this.barWidth / 2]);
points.push([gridData[i][0], base - this.barWidth / 2]);
points.push([xstart, base - this.barWidth / 2]);
points.push([xstart, base + this.barWidth / 2]);
}
this._barPoints.push(points);
// now draw the shadows if not stacked.
// for stacked plots, they are predrawn by drawShadow
if (shadow && !this._stack) {
var sopts = $.extend(true, {}, opts);
delete sopts.fillStyle;
this.renderer.shadowRenderer.draw(ctx, points, sopts);
}
var clr = opts.fillStyle || this.color;
this._dataColors.push(clr);
this.renderer.shapeRenderer.draw(ctx, points, opts);
}
}
}
if (this.highlightColors.length == 0) {
this.highlightColors = $.jqplot.computeHighlightColors(this._dataColors);
}
else if (typeof(this.highlightColors) == 'string') {
var temp = this.highlightColors;
this.highlightColors = [];
for (var i=0; i<this._dataColors.length; i++) {
this.highlightColors.push(temp);
}
}
};
// for stacked plots, shadows will be pre drawn by drawShadow.
$.jqplot.BarRenderer.prototype.drawShadow = function(ctx, gridData, options, plot) {
var i;
var opts = (options != undefined) ? options : {};
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
var xaxis = this.xaxis;
var yaxis = this.yaxis;
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var pointx, points, pointy, nvals, nseries, pos;
if (this._stack && this.shadow) {
if (this.barWidth == null) {
this.renderer.setBarWidth.call(this);
}
var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
nvals = temp[0];
nseries = temp[1];
pos = temp[2];
if (this._stack) {
this._barNudge = 0;
}
else {
this._barNudge = (-Math.abs(nseries/2 - 0.5) + pos) * (this.barWidth + this.barPadding);
}
if (showLine) {
if (this.barDirection == 'vertical') {
for (var i=0; i<gridData.length; i++) {
if (this.data[i][1] == null) {
continue;
}
points = [];
var base = gridData[i][0] + this._barNudge;
var ystart;
if (this._stack && this._prevGridData.length) {
ystart = getStart(this.index, i, this._plotData[i][1], plot, 'y');
}
else {
if (this.fillToZero) {
ystart = this._yaxis.series_u2p(0);
}
else {
ystart = ctx.canvas.height;
}
}
points.push([base-this.barWidth/2, ystart]);
points.push([base-this.barWidth/2, gridData[i][1]]);
points.push([base+this.barWidth/2, gridData[i][1]]);
points.push([base+this.barWidth/2, ystart]);
this.renderer.shadowRenderer.draw(ctx, points, opts);
}
}
else if (this.barDirection == 'horizontal'){
for (var i=0; i<gridData.length; i++) {
if (this.data[i][0] == null) {
continue;
}
points = [];
var base = gridData[i][1] - this._barNudge;
var xstart;
if (this._stack && this._prevGridData.length) {
xstart = getStart(this.index, i, this._plotData[i][0], plot, 'x');
}
else {
if (this.fillToZero) {
xstart = this._xaxis.series_u2p(0);
}
else {
xstart = 0;
}
}
points.push([xstart, base+this.barWidth/2]);
points.push([gridData[i][0], base+this.barWidth/2]);
points.push([gridData[i][0], base-this.barWidth/2]);
points.push([xstart, base-this.barWidth/2]);
this.renderer.shadowRenderer.draw(ctx, points, opts);
}
}
}
}
};
function postInit(target, data, options) {
for (var i=0; i<this.series.length; i++) {
if (this.series[i].renderer.constructor == $.jqplot.BarRenderer) {
// don't allow mouseover and mousedown at same time.
if (this.series[i].highlightMouseOver) {
this.series[i].highlightMouseDown = false;
}
}
}
}
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
function postPlotDraw() {
// Memory Leaks patch
if (this.plugins.barRenderer && this.plugins.barRenderer.highlightCanvas) {
this.plugins.barRenderer.highlightCanvas.resetCanvas();
this.plugins.barRenderer.highlightCanvas = null;
}
this.plugins.barRenderer = {highlightedSeriesIndex:null};
this.plugins.barRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
this.eventCanvas._elem.before(this.plugins.barRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-barRenderer-highlight-canvas', this._plotDimensions, this));
this.plugins.barRenderer.highlightCanvas.setContext();
this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
}
function highlight (plot, sidx, pidx, points) {
var s = plot.series[sidx];
var canvas = plot.plugins.barRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
s._highlightedPoint = pidx;
plot.plugins.barRenderer.highlightedSeriesIndex = sidx;
var opts = {fillStyle: s.highlightColors[pidx]};
s.renderer.shapeRenderer.draw(canvas._ctx, points, opts);
canvas = null;
}
function unhighlight (plot) {
var canvas = plot.plugins.barRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
for (var i=0; i<plot.series.length; i++) {
plot.series[i]._highlightedPoint = null;
}
plot.plugins.barRenderer.highlightedSeriesIndex = null;
plot.target.trigger('jqplotDataUnhighlight');
canvas = null;
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt1 = jQuery.Event('jqplotDataMouseOver');
evt1.pageX = ev.pageX;
evt1.pageY = ev.pageY;
plot.target.trigger(evt1, ins);
if (plot.series[ins[0]].show && plot.series[ins[0]].highlightMouseOver &&
!(ins[0] == plot.plugins.barRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.barRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
var idx = plot.plugins.barRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
}
function handleClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt = jQuery.Event('jqplotDataClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var idx = plot.plugins.barRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
var evt = jQuery.Event('jqplotDataRightClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
})(jQuery);

View File

@@ -0,0 +1,235 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.BlockRenderer
* Plugin renderer to draw a x-y block chart. A Block chart has data points displayed as
* colored squares with a text label inside. Data must be supplied in the form:
*
* > [[x1, y1, "label 1", {css}], [x2, y2, "label 2", {css}], ...]
*
* The label and css object are optional. If the label is ommitted, the
* box will collapse unless a css height and/or width is specified.
*
* The css object is an object specifying css properties
* such as:
*
* > {background:'#4f98a5', border:'3px solid gray', padding:'1px'}
*
* Note that css properties specified with the data point override defaults
* specified with the series.
*
*/
$.jqplot.BlockRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.BlockRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.BlockRenderer.prototype.constructor = $.jqplot.BlockRenderer;
// called with scope of a series
$.jqplot.BlockRenderer.prototype.init = function(options) {
// Group: Properties
//
// prop: css
// default css styles that will be applied to all data blocks.
// these values will be overridden by css styles supplied with the
// individulal data points.
this.css = {padding:'2px', border:'1px solid #999', textAlign:'center'};
// prop: escapeHtml
// true to escape html in the box label.
this.escapeHtml = false;
// prop: insertBreaks
// true to turn spaces in data block label into html breaks <br />.
this.insertBreaks = true;
// prop: varyBlockColors
// true to vary the color of each block in this series according to
// the seriesColors array. False to set each block to the color
// specified on this series. This has no effect if a css background color
// option is specified in the renderer css options.
this.varyBlockColors = false;
$.extend(true, this, options);
if (this.css.backgroundColor) {
this.color = this.css.backgroundColor;
}
else if (this.css.background) {
this.color = this.css.background;
}
else if (!this.varyBlockColors) {
this.css.background = this.color;
}
this.canvas = new $.jqplot.BlockCanvas();
this.shadowCanvas = new $.jqplot.BlockCanvas();
this.canvas._plotDimensions = this._plotDimensions;
this.shadowCanvas._plotDimensions = this._plotDimensions;
this._type = 'block';
// group: Methods
//
// Method: moveBlock
// Moves an individual block. More efficient than redrawing
// the whole series by calling plot.drawSeries().
// Properties:
// idx - the 0 based index of the block or point in this series.
// x - the x coordinate in data units (value on x axis) to move the block to.
// y - the y coordinate in data units (value on the y axis) to move the block to.
// duration - optional parameter to create an animated movement. Can be a
// number (higher is slower animation) or 'fast', 'normal' or 'slow'. If not
// provided, the element is moved without any animation.
this.moveBlock = function (idx, x, y, duration) {
// update plotData, stackData, data and gridData
// x and y are in data coordinates.
var el = this.canvas._elem.children(':eq('+idx+')');
this.data[idx][0] = x;
this.data[idx][1] = y;
this._plotData[idx][0] = x;
this._plotData[idx][1] = y;
this._stackData[idx][0] = x;
this._stackData[idx][1] = y;
this.gridData[idx][0] = this._xaxis.series_u2p(x);
this.gridData[idx][1] = this._yaxis.series_u2p(y);
var w = el.outerWidth();
var h = el.outerHeight();
var left = this.gridData[idx][0] - w/2 + 'px';
var top = this.gridData[idx][1] - h/2 + 'px';
if (duration) {
if (parseInt(duration, 10)) {
duration = parseInt(duration, 10);
}
el.animate({left:left, top:top}, duration);
}
else {
el.css({left:left, top:top});
}
el = null;
};
};
// called with scope of series
$.jqplot.BlockRenderer.prototype.draw = function (ctx, gd, options) {
if (this.plugins.pointLabels) {
this.plugins.pointLabels.show = false;
}
var i, el, d, gd, t, css, w, h, left, top;
var opts = (options != undefined) ? options : {};
var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
this.canvas._elem.empty();
for (i=0; i<this.gridData.length; i++) {
d = this.data[i];
gd = this.gridData[i];
t = '';
css = {};
if (typeof d[2] == 'string') {
t = d[2];
}
else if (typeof d[2] == 'object') {
css = d[2];
}
if (typeof d[3] == 'object') {
css = d[3];
}
if (this.insertBreaks){
t = t.replace(/ /g, '<br />');
}
css = $.extend(true, {}, this.css, css);
// create a div
el = $('<div style="position:absolute;margin-left:auto;margin-right:auto;"></div>');
this.canvas._elem.append(el);
// set text
this.escapeHtml ? el.text(t) : el.html(t);
// style it
// remove styles we don't want overridden.
delete css.position;
delete css.marginRight;
delete css.marginLeft;
if (!css.background && !css.backgroundColor && !css.backgroundImage){
css.background = colorGenerator.next();
}
el.css(css);
w = el.outerWidth();
h = el.outerHeight();
left = gd[0] - w/2 + 'px';
top = gd[1] - h/2 + 'px';
el.css({left:left, top:top});
el = null;
}
};
$.jqplot.BlockCanvas = function() {
$.jqplot.ElemContainer.call(this);
this._ctx;
};
$.jqplot.BlockCanvas.prototype = new $.jqplot.ElemContainer();
$.jqplot.BlockCanvas.prototype.constructor = $.jqplot.BlockCanvas;
$.jqplot.BlockCanvas.prototype.createElement = function(offsets, clss, plotDimensions) {
this._offsets = offsets;
var klass = 'jqplot-blockCanvas';
if (clss != undefined) {
klass = clss;
}
var elem;
// if this canvas already has a dom element, don't make a new one.
if (this._elem) {
elem = this._elem.get(0);
}
else {
elem = document.createElement('div');
}
// if new plotDimensions supplied, use them.
if (plotDimensions != undefined) {
this._plotDimensions = plotDimensions;
}
var w = this._plotDimensions.width - this._offsets.left - this._offsets.right + 'px';
var h = this._plotDimensions.height - this._offsets.top - this._offsets.bottom + 'px';
this._elem = $(elem);
this._elem.css({ position: 'absolute', width:w, height:h, left: this._offsets.left, top: this._offsets.top });
this._elem.addClass(klass);
return this._elem;
};
$.jqplot.BlockCanvas.prototype.setContext = function() {
this._ctx = {
canvas:{
width:0,
height:0
},
clearRect:function(){return null;}
};
return this._ctx;
};
})(jQuery);

View File

@@ -0,0 +1,759 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
var arrayMax = function( array ){
return Math.max.apply( Math, array );
};
var arrayMin = function( array ){
return Math.min.apply( Math, array );
};
/**
* Class: $.jqplot.BubbleRenderer
* Plugin renderer to draw a bubble chart. A Bubble chart has data points displayed as
* colored circles with an optional text label inside. To use
* the bubble renderer, you must include the bubble renderer like:
*
* > <script language="javascript" type="text/javascript" src="../src/plugins/jqplot.bubbleRenderer.js"></script>
*
* Data must be supplied in
* the form:
*
* > [[x1, y1, r1, <label or {label:'text', color:color}>], ...]
*
* where the label or options
* object is optional.
*
* Note that all bubble colors will be the same
* unless the "varyBubbleColors" option is set to true. Colors can be specified in the data array
* or in the seriesColors array option on the series. If no colors are defined, the default jqPlot
* series of 16 colors are used. Colors are automatically cycled around again if there are more
* bubbles than colors.
*
* Bubbles are autoscaled by default to fit within the chart area while maintaining
* relative sizes. If the "autoscaleBubbles" option is set to false, the r(adius) values
* in the data array a treated as literal pixel values for the radii of the bubbles.
*
* Properties are passed into the bubble renderer in the rendererOptions object of
* the series options like:
*
* > seriesDefaults: {
* > renderer: $.jqplot.BubbleRenderer,
* > rendererOptions: {
* > bubbleAlpha: 0.7,
* > varyBubbleColors: false
* > }
* > }
*
*/
$.jqplot.BubbleRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.BubbleRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.BubbleRenderer.prototype.constructor = $.jqplot.BubbleRenderer;
// called with scope of a series
$.jqplot.BubbleRenderer.prototype.init = function(options, plot) {
// Group: Properties
//
// prop: varyBubbleColors
// True to vary the color of each bubble in this series according to
// the seriesColors array. False to set each bubble to the color
// specified on this series. This has no effect if a css background color
// option is specified in the renderer css options.
this.varyBubbleColors = true;
// prop: autoscaleBubbles
// True to scale the bubble radius based on plot size.
// False will use the radius value as provided as a raw pixel value for
// bubble radius.
this.autoscaleBubbles = true;
// prop: autoscaleMultiplier
// Multiplier the bubble size if autoscaleBubbles is true.
this.autoscaleMultiplier = 1.0;
// prop: autoscalePointsFactor
// Factor which decreases bubble size based on how many bubbles on on the chart.
// 0 means no adjustment for number of bubbles. Negative values will decrease
// size of bubbles as more bubbles are added. Values between 0 and -0.2
// should work well.
this.autoscalePointsFactor = -0.07;
// prop: escapeHtml
// True to escape html in bubble label text.
this.escapeHtml = true;
// prop: highlightMouseOver
// True to highlight bubbles when moused over.
// This must be false to enable highlightMouseDown to highlight when clicking on a slice.
this.highlightMouseOver = true;
// prop: highlightMouseDown
// True to highlight when a mouse button is pressed over a bubble.
// This will be disabled if highlightMouseOver is true.
this.highlightMouseDown = false;
// prop: highlightColors
// An array of colors to use when highlighting a slice. Calculated automatically
// if not supplied.
this.highlightColors = [];
// prop: bubbleAlpha
// Alpha transparency to apply to all bubbles in this series.
this.bubbleAlpha = 1.0;
// prop: highlightAlpha
// Alpha transparency to apply when highlighting bubble.
// Set to value of bubbleAlpha by default.
this.highlightAlpha = null;
// prop: bubbleGradients
// True to color the bubbles with gradient fills instead of flat colors.
// NOT AVAILABLE IN IE due to lack of excanvas support for radial gradient fills.
// will be ignored in IE.
this.bubbleGradients = false;
// prop: showLabels
// True to show labels on bubbles (if any), false to not show.
this.showLabels = true;
// array of [point index, radius] which will be sorted in descending order to plot
// largest points below smaller points.
this.radii = [];
this.maxRadius = 0;
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// array of jQuery labels.
this.labels = [];
this.bubbleCanvases = [];
this._type = 'bubble';
// if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
if (options.highlightMouseDown && options.highlightMouseOver == null) {
options.highlightMouseOver = false;
}
$.extend(true, this, options);
if (this.highlightAlpha == null) {
this.highlightAlpha = this.bubbleAlpha;
if (this.bubbleGradients) {
this.highlightAlpha = 0.35;
}
}
this.autoscaleMultiplier = this.autoscaleMultiplier * Math.pow(this.data.length, this.autoscalePointsFactor);
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// adjust the series colors for options colors passed in with data or for alpha.
// note, this can leave undefined holes in the seriesColors array.
var comps;
for (var i=0; i<this.data.length; i++) {
var color = null;
var d = this.data[i];
this.maxRadius = Math.max(this.maxRadius, d[2]);
if (d[3]) {
if (typeof(d[3]) == 'object') {
color = d[3]['color'];
}
}
if (color == null) {
if (this.seriesColors[i] != null) {
color = this.seriesColors[i];
}
}
if (color && this.bubbleAlpha < 1.0) {
comps = $.jqplot.getColorComponents(color);
color = 'rgba('+comps[0]+', '+comps[1]+', '+comps[2]+', '+this.bubbleAlpha+')';
}
if (color) {
this.seriesColors[i] = color;
}
}
if (!this.varyBubbleColors) {
this.seriesColors = [this.color];
}
this.colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
// set highlight colors if none provided
if (this.highlightColors.length == 0) {
for (var i=0; i<this.seriesColors.length; i++){
var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var sum = newrgb[0] + newrgb[1] + newrgb[2];
for (var j=0; j<3; j++) {
// when darkening, lowest color component can be is 60.
newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
newrgb[j] = parseInt(newrgb[j], 10);
}
this.highlightColors.push('rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+', '+this.highlightAlpha+')');
}
}
this.highlightColorGenerator = new $.jqplot.ColorGenerator(this.highlightColors);
var sopts = {fill:true, isarc:true, angle:this.shadowAngle, alpha:this.shadowAlpha, closePath:true};
this.renderer.shadowRenderer.init(sopts);
this.canvas = new $.jqplot.DivCanvas();
this.canvas._plotDimensions = this._plotDimensions;
plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
plot.postDrawHooks.addOnce(postPlotDraw);
};
// converts the user data values to grid coordinates and stores them
// in the gridData array.
// Called with scope of a series.
$.jqplot.BubbleRenderer.prototype.setGridData = function(plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var data = this._plotData;
this.gridData = [];
var radii = [];
this.radii = [];
var dim = Math.min(plot._height, plot._width);
for (var i=0; i<this.data.length; i++) {
if (data[i] != null) {
this.gridData.push([xp.call(this._xaxis, data[i][0]), yp.call(this._yaxis, data[i][1]), data[i][2]]);
this.radii.push([i, data[i][2]]);
radii.push(data[i][2]);
}
}
var r, val, maxr = this.maxRadius = arrayMax(radii);
var l = this.gridData.length;
if (this.autoscaleBubbles) {
for (var i=0; i<l; i++) {
val = radii[i]/maxr;
r = this.autoscaleMultiplier * dim / 6;
this.gridData[i][2] = r * val;
}
}
this.radii.sort(function(a, b) { return b[1] - a[1]; });
};
// converts any arbitrary data values to grid coordinates and
// returns them. This method exists so that plugins can use a series'
// linerenderer to generate grid data points without overwriting the
// grid data associated with that series.
// Called with scope of a series.
$.jqplot.BubbleRenderer.prototype.makeGridData = function(data, plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var gd = [];
var radii = [];
this.radii = [];
var dim = Math.min(plot._height, plot._width);
for (var i=0; i<data.length; i++) {
if (data[i] != null) {
gd.push([xp.call(this._xaxis, data[i][0]), yp.call(this._yaxis, data[i][1]), data[i][2]]);
radii.push(data[i][2]);
this.radii.push([i, data[i][2]]);
}
}
var r, val, maxr = this.maxRadius = arrayMax(radii);
var l = this.gridData.length;
if (this.autoscaleBubbles) {
for (var i=0; i<l; i++) {
val = radii[i]/maxr;
r = this.autoscaleMultiplier * dim / 6;
gd[i][2] = r * val;
}
}
this.radii.sort(function(a, b) { return b[1] - a[1]; });
return gd;
};
// called with scope of series
$.jqplot.BubbleRenderer.prototype.draw = function (ctx, gd, options) {
if (this.plugins.pointLabels) {
this.plugins.pointLabels.show = false;
}
var opts = (options != undefined) ? options : {};
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
this.canvas._elem.empty();
for (var i=0; i<this.radii.length; i++) {
var idx = this.radii[i][0];
var t=null;
var color = null;
var el = null;
var tel = null;
var d = this.data[idx];
var gd = this.gridData[idx];
if (d[3]) {
if (typeof(d[3]) == 'object') {
t = d[3]['label'];
}
else if (typeof(d[3]) == 'string') {
t = d[3];
}
}
// color = (this.varyBubbleColors) ? this.colorGenerator.get(idx) : this.color;
color = this.colorGenerator.get(idx);
// If we're drawing a shadow, expand the canvas dimensions to accomodate.
var canvasRadius = gd[2];
var offset, depth;
if (this.shadow) {
offset = (0.7 + gd[2]/40).toFixed(1);
depth = 1 + Math.ceil(gd[2]/15);
canvasRadius += offset*depth;
}
this.bubbleCanvases[idx] = new $.jqplot.BubbleCanvas();
this.canvas._elem.append(this.bubbleCanvases[idx].createElement(gd[0], gd[1], canvasRadius));
this.bubbleCanvases[idx].setContext();
var ctx = this.bubbleCanvases[idx]._ctx;
var x = ctx.canvas.width/2;
var y = ctx.canvas.height/2;
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [x, y, gd[2], 0, 2*Math.PI], {offset: offset, depth: depth});
}
this.bubbleCanvases[idx].draw(gd[2], color, this.bubbleGradients, this.shadowAngle/180*Math.PI);
// now draw label.
if (t && this.showLabels) {
tel = $('<div style="position:absolute;" class="jqplot-bubble-label"></div>');
if (this.escapeHtml) {
tel.text(t);
}
else {
tel.html(t);
}
this.canvas._elem.append(tel);
var h = $(tel).outerHeight();
var w = $(tel).outerWidth();
var top = gd[1] - 0.5*h;
var left = gd[0] - 0.5*w;
tel.css({top: top, left: left});
this.labels[idx] = $(tel);
}
}
};
$.jqplot.DivCanvas = function() {
$.jqplot.ElemContainer.call(this);
this._ctx;
};
$.jqplot.DivCanvas.prototype = new $.jqplot.ElemContainer();
$.jqplot.DivCanvas.prototype.constructor = $.jqplot.DivCanvas;
$.jqplot.DivCanvas.prototype.createElement = function(offsets, clss, plotDimensions) {
this._offsets = offsets;
var klass = 'jqplot-DivCanvas';
if (clss != undefined) {
klass = clss;
}
var elem;
// if this canvas already has a dom element, don't make a new one.
if (this._elem) {
elem = this._elem.get(0);
}
else {
elem = document.createElement('div');
}
// if new plotDimensions supplied, use them.
if (plotDimensions != undefined) {
this._plotDimensions = plotDimensions;
}
var w = this._plotDimensions.width - this._offsets.left - this._offsets.right + 'px';
var h = this._plotDimensions.height - this._offsets.top - this._offsets.bottom + 'px';
this._elem = $(elem);
this._elem.css({ position: 'absolute', width:w, height:h, left: this._offsets.left, top: this._offsets.top });
this._elem.addClass(klass);
return this._elem;
};
$.jqplot.DivCanvas.prototype.setContext = function() {
this._ctx = {
canvas:{
width:0,
height:0
},
clearRect:function(){return null;}
};
return this._ctx;
};
$.jqplot.BubbleCanvas = function() {
$.jqplot.ElemContainer.call(this);
this._ctx;
};
$.jqplot.BubbleCanvas.prototype = new $.jqplot.ElemContainer();
$.jqplot.BubbleCanvas.prototype.constructor = $.jqplot.BubbleCanvas;
// initialize with the x,y pont of bubble center and the bubble radius.
$.jqplot.BubbleCanvas.prototype.createElement = function(x, y, r) {
var klass = 'jqplot-bubble-point';
var elem;
// if this canvas already has a dom element, don't make a new one.
if (this._elem) {
elem = this._elem.get(0);
}
else {
elem = document.createElement('canvas');
}
elem.width = (r != null) ? 2*r : elem.width;
elem.height = (r != null) ? 2*r : elem.height;
this._elem = $(elem);
var l = (x != null && r != null) ? x - r : this._elem.css('left');
var t = (y != null && r != null) ? y - r : this._elem.css('top');
this._elem.css({ position: 'absolute', left: l, top: t });
this._elem.addClass(klass);
if ($.jqplot.use_excanvas) {
window.G_vmlCanvasManager.init_(document);
elem = window.G_vmlCanvasManager.initElement(elem);
}
return this._elem;
};
$.jqplot.BubbleCanvas.prototype.draw = function(r, color, gradients, angle) {
var ctx = this._ctx;
// r = Math.floor(r*1.04);
// var x = Math.round(ctx.canvas.width/2);
// var y = Math.round(ctx.canvas.height/2);
var x = ctx.canvas.width/2;
var y = ctx.canvas.height/2;
ctx.save();
if (gradients && !$.jqplot.use_excanvas) {
r = r*1.04;
var comps = $.jqplot.getColorComponents(color);
var colorinner = 'rgba('+Math.round(comps[0]+0.8*(255-comps[0]))+', '+Math.round(comps[1]+0.8*(255-comps[1]))+', '+Math.round(comps[2]+0.8*(255-comps[2]))+', '+comps[3]+')';
var colorend = 'rgba('+comps[0]+', '+comps[1]+', '+comps[2]+', 0)';
// var rinner = Math.round(0.35 * r);
// var xinner = Math.round(x - Math.cos(angle) * 0.33 * r);
// var yinner = Math.round(y - Math.sin(angle) * 0.33 * r);
var rinner = 0.35 * r;
var xinner = x - Math.cos(angle) * 0.33 * r;
var yinner = y - Math.sin(angle) * 0.33 * r;
var radgrad = ctx.createRadialGradient(xinner, yinner, rinner, x, y, r);
radgrad.addColorStop(0, colorinner);
radgrad.addColorStop(0.93, color);
radgrad.addColorStop(0.96, colorend);
radgrad.addColorStop(1, colorend);
// radgrad.addColorStop(.98, colorend);
ctx.fillStyle = radgrad;
ctx.fillRect(0,0, ctx.canvas.width, ctx.canvas.height);
}
else {
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.beginPath();
var ang = 2*Math.PI;
ctx.arc(x, y, r, 0, ang, 0);
ctx.closePath();
ctx.fill();
}
ctx.restore();
};
$.jqplot.BubbleCanvas.prototype.setContext = function() {
this._ctx = this._elem.get(0).getContext("2d");
return this._ctx;
};
$.jqplot.BubbleAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
};
$.jqplot.BubbleAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.BubbleAxisRenderer.prototype.constructor = $.jqplot.BubbleAxisRenderer;
// called with scope of axis object.
$.jqplot.BubbleAxisRenderer.prototype.init = function(options){
$.extend(true, this, options);
var db = this._dataBounds;
var minsidx = 0,
minpidx = 0,
maxsidx = 0,
maxpidx = 0,
maxr = 0,
minr = 0,
minMaxRadius = 0,
maxMaxRadius = 0,
maxMult = 0,
minMult = 0;
// Go through all the series attached to this axis and find
// the min/max bounds for this axis.
for (var i=0; i<this._series.length; i++) {
var s = this._series[i];
var d = s._plotData;
for (var j=0; j<d.length; j++) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
if (d[j][0] < db.min || db.min == null) {
db.min = d[j][0];
minsidx=i;
minpidx=j;
minr = d[j][2];
minMaxRadius = s.maxRadius;
minMult = s.autoscaleMultiplier;
}
if (d[j][0] > db.max || db.max == null) {
db.max = d[j][0];
maxsidx=i;
maxpidx=j;
maxr = d[j][2];
maxMaxRadius = s.maxRadius;
maxMult = s.autoscaleMultiplier;
}
}
else {
if (d[j][1] < db.min || db.min == null) {
db.min = d[j][1];
minsidx=i;
minpidx=j;
minr = d[j][2];
minMaxRadius = s.maxRadius;
minMult = s.autoscaleMultiplier;
}
if (d[j][1] > db.max || db.max == null) {
db.max = d[j][1];
maxsidx=i;
maxpidx=j;
maxr = d[j][2];
maxMaxRadius = s.maxRadius;
maxMult = s.autoscaleMultiplier;
}
}
}
}
var minRatio = minr/minMaxRadius;
var maxRatio = maxr/maxMaxRadius;
// need to estimate the effect of the radius on total axis span and adjust axis accordingly.
var span = db.max - db.min;
// var dim = (this.name == 'xaxis' || this.name == 'x2axis') ? this._plotDimensions.width : this._plotDimensions.height;
var dim = Math.min(this._plotDimensions.width, this._plotDimensions.height);
var minfact = minRatio * minMult/3 * span;
var maxfact = maxRatio * maxMult/3 * span;
db.max += maxfact;
db.min -= minfact;
};
function highlight (plot, sidx, pidx) {
plot.plugins.bubbleRenderer.highlightLabelCanvas.empty();
var s = plot.series[sidx];
var canvas = plot.plugins.bubbleRenderer.highlightCanvas;
var ctx = canvas._ctx;
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
s._highlightedPoint = pidx;
plot.plugins.bubbleRenderer.highlightedSeriesIndex = sidx;
var color = s.highlightColorGenerator.get(pidx);
var x = s.gridData[pidx][0],
y = s.gridData[pidx][1],
r = s.gridData[pidx][2];
ctx.save();
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(x, y, r, 0, 2*Math.PI, 0);
ctx.closePath();
ctx.fill();
ctx.restore();
// bring label to front
if (s.labels[pidx]) {
plot.plugins.bubbleRenderer.highlightLabel = s.labels[pidx].clone();
plot.plugins.bubbleRenderer.highlightLabel.appendTo(plot.plugins.bubbleRenderer.highlightLabelCanvas);
plot.plugins.bubbleRenderer.highlightLabel.addClass('jqplot-bubble-label-highlight');
}
}
function unhighlight (plot) {
var canvas = plot.plugins.bubbleRenderer.highlightCanvas;
var sidx = plot.plugins.bubbleRenderer.highlightedSeriesIndex;
plot.plugins.bubbleRenderer.highlightLabelCanvas.empty();
canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
for (var i=0; i<plot.series.length; i++) {
plot.series[i]._highlightedPoint = null;
}
plot.plugins.bubbleRenderer.highlightedSeriesIndex = null;
plot.target.trigger('jqplotDataUnhighlight');
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var si = neighbor.seriesIndex;
var pi = neighbor.pointIndex;
var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
var evt1 = jQuery.Event('jqplotDataMouseOver');
evt1.pageX = ev.pageX;
evt1.pageY = ev.pageY;
plot.target.trigger(evt1, ins);
if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.bubbleRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var si = neighbor.seriesIndex;
var pi = neighbor.pointIndex;
var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.bubbleRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
var idx = plot.plugins.bubbleRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
}
function handleClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var si = neighbor.seriesIndex;
var pi = neighbor.pointIndex;
var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
var evt = jQuery.Event('jqplotDataClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var si = neighbor.seriesIndex;
var pi = neighbor.pointIndex;
var ins = [si, pi, neighbor.data, plot.series[si].gridData[pi][2]];
var idx = plot.plugins.bubbleRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
var evt = jQuery.Event('jqplotDataRightClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
function postPlotDraw() {
// Memory Leaks patch
if (this.plugins.bubbleRenderer && this.plugins.bubbleRenderer.highlightCanvas) {
this.plugins.bubbleRenderer.highlightCanvas.resetCanvas();
this.plugins.bubbleRenderer.highlightCanvas = null;
}
this.plugins.bubbleRenderer = {highlightedSeriesIndex:null};
this.plugins.bubbleRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
this.plugins.bubbleRenderer.highlightLabel = null;
this.plugins.bubbleRenderer.highlightLabelCanvas = $('<div style="position:absolute;"></div>');
var top = this._gridPadding.top;
var left = this._gridPadding.left;
var width = this._plotDimensions.width - this._gridPadding.left - this._gridPadding.right;
var height = this._plotDimensions.height - this._gridPadding.top - this._gridPadding.bottom;
this.plugins.bubbleRenderer.highlightLabelCanvas.css({top:top, left:left, width:width+'px', height:height+'px'});
this.eventCanvas._elem.before(this.plugins.bubbleRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-bubbleRenderer-highlight-canvas', this._plotDimensions, this));
this.eventCanvas._elem.before(this.plugins.bubbleRenderer.highlightLabelCanvas);
var hctx = this.plugins.bubbleRenderer.highlightCanvas.setContext();
}
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = options.axesDefaults || {};
options.seriesDefaults = options.seriesDefaults || {};
// only set these if there is a Bubble series
var setopts = false;
if (options.seriesDefaults.renderer == $.jqplot.BubbleRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer == $.jqplot.BubbleRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.BubbleAxisRenderer;
options.sortData = false;
}
}
$.jqplot.preInitHooks.push(preInit);
})(jQuery);

View File

@@ -0,0 +1,203 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.CanvasAxisLabelRenderer
* Renderer to draw axis labels with a canvas element to support advanced
* featrues such as rotated text. This renderer uses a separate rendering engine
* to draw the text on the canvas. Two modes of rendering the text are available.
* If the browser has native font support for canvas fonts (currently Mozila 3.5
* and Safari 4), you can enable text rendering with the canvas fillText method.
* You do so by setting the "enableFontSupport" option to true.
*
* Browsers lacking native font support will have the text drawn on the canvas
* using the Hershey font metrics. Even if the "enableFontSupport" option is true
* non-supporting browsers will still render with the Hershey font.
*
*/
$.jqplot.CanvasAxisLabelRenderer = function(options) {
// Group: Properties
// prop: angle
// angle of text, measured clockwise from x axis.
this.angle = 0;
// name of the axis associated with this tick
this.axis;
// prop: show
// whether or not to show the tick (mark and label).
this.show = true;
// prop: showLabel
// whether or not to show the label.
this.showLabel = true;
// prop: label
// label for the axis.
this.label = '';
// prop: fontFamily
// CSS spec for the font-family css attribute.
// Applies only to browsers supporting native font rendering in the
// canvas tag. Currently Mozilla 3.5 and Safari 4.
this.fontFamily = '"Trebuchet MS", Arial, Helvetica, sans-serif';
// prop: fontSize
// CSS spec for font size.
this.fontSize = '11pt';
// prop: fontWeight
// CSS spec for fontWeight: normal, bold, bolder, lighter or a number 100 - 900
this.fontWeight = 'normal';
// prop: fontStretch
// Multiplier to condense or expand font width.
// Applies only to browsers which don't support canvas native font rendering.
this.fontStretch = 1.0;
// prop: textColor
// css spec for the color attribute.
this.textColor = '#666666';
// prop: enableFontSupport
// true to turn on native canvas font support in Mozilla 3.5+ and Safari 4+.
// If true, label will be drawn with canvas tag native support for fonts.
// If false, label will be drawn with Hershey font metrics.
this.enableFontSupport = true;
// prop: pt2px
// Point to pixel scaling factor, used for computing height of bounding box
// around a label. The labels text renderer has a default setting of 1.4, which
// should be suitable for most fonts. Leave as null to use default. If tops of
// letters appear clipped, increase this. If bounding box seems too big, decrease.
// This is an issue only with the native font renderering capabilities of Mozilla
// 3.5 and Safari 4 since they do not provide a method to determine the font height.
this.pt2px = null;
this._elem;
this._ctx;
this._plotWidth;
this._plotHeight;
this._plotDimensions = {height:null, width:null};
$.extend(true, this, options);
if (options.angle == null && this.axis != 'xaxis' && this.axis != 'x2axis') {
this.angle = -90;
}
var ropts = {fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily};
if (this.pt2px) {
ropts.pt2px = this.pt2px;
}
if (this.enableFontSupport) {
if ($.jqplot.support_canvas_text()) {
this._textRenderer = new $.jqplot.CanvasFontRenderer(ropts);
}
else {
this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
}
}
else {
this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
}
};
$.jqplot.CanvasAxisLabelRenderer.prototype.init = function(options) {
$.extend(true, this, options);
this._textRenderer.init({fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily});
};
// return width along the x axis
// will check first to see if an element exists.
// if not, will return the computed text box width.
$.jqplot.CanvasAxisLabelRenderer.prototype.getWidth = function(ctx) {
if (this._elem) {
return this._elem.outerWidth(true);
}
else {
var tr = this._textRenderer;
var l = tr.getWidth(ctx);
var h = tr.getHeight(ctx);
var w = Math.abs(Math.sin(tr.angle)*h) + Math.abs(Math.cos(tr.angle)*l);
return w;
}
};
// return height along the y axis.
$.jqplot.CanvasAxisLabelRenderer.prototype.getHeight = function(ctx) {
if (this._elem) {
return this._elem.outerHeight(true);
}
else {
var tr = this._textRenderer;
var l = tr.getWidth(ctx);
var h = tr.getHeight(ctx);
var w = Math.abs(Math.cos(tr.angle)*h) + Math.abs(Math.sin(tr.angle)*l);
return w;
}
};
$.jqplot.CanvasAxisLabelRenderer.prototype.getAngleRad = function() {
var a = this.angle * Math.PI/180;
return a;
};
$.jqplot.CanvasAxisLabelRenderer.prototype.draw = function(ctx, plot) {
// Memory Leaks patch
if (this._elem) {
if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
window.G_vmlCanvasManager.uninitElement(this._elem.get(0));
}
this._elem.emptyForce();
this._elem = null;
}
// create a canvas here, but can't draw on it untill it is appended
// to dom for IE compatability.
var elem = plot.canvasManager.getCanvas();
this._textRenderer.setText(this.label, ctx);
var w = this.getWidth(ctx);
var h = this.getHeight(ctx);
elem.width = w;
elem.height = h;
elem.style.width = w;
elem.style.height = h;
elem = plot.canvasManager.initCanvas(elem);
this._elem = $(elem);
this._elem.css({ position: 'absolute'});
this._elem.addClass('jqplot-'+this.axis+'-label');
elem = null;
return this._elem;
};
$.jqplot.CanvasAxisLabelRenderer.prototype.pack = function() {
this._textRenderer.draw(this._elem.get(0).getContext("2d"), this.label);
};
})(jQuery);

View File

@@ -0,0 +1,253 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.CanvasAxisTickRenderer
* Renderer to draw axis ticks with a canvas element to support advanced
* featrues such as rotated text. This renderer uses a separate rendering engine
* to draw the text on the canvas. Two modes of rendering the text are available.
* If the browser has native font support for canvas fonts (currently Mozila 3.5
* and Safari 4), you can enable text rendering with the canvas fillText method.
* You do so by setting the "enableFontSupport" option to true.
*
* Browsers lacking native font support will have the text drawn on the canvas
* using the Hershey font metrics. Even if the "enableFontSupport" option is true
* non-supporting browsers will still render with the Hershey font.
*/
$.jqplot.CanvasAxisTickRenderer = function(options) {
// Group: Properties
// prop: mark
// tick mark on the axis. One of 'inside', 'outside', 'cross', '' or null.
this.mark = 'outside';
// prop: showMark
// whether or not to show the mark on the axis.
this.showMark = true;
// prop: showGridline
// whether or not to draw the gridline on the grid at this tick.
this.showGridline = true;
// prop: isMinorTick
// if this is a minor tick.
this.isMinorTick = false;
// prop: angle
// angle of text, measured clockwise from x axis.
this.angle = 0;
// prop: markSize
// Length of the tick marks in pixels. For 'cross' style, length
// will be stoked above and below axis, so total length will be twice this.
this.markSize = 4;
// prop: show
// whether or not to show the tick (mark and label).
this.show = true;
// prop: showLabel
// whether or not to show the label.
this.showLabel = true;
// prop: labelPosition
// 'auto', 'start', 'middle' or 'end'.
// Whether tick label should be positioned so the start, middle, or end
// of the tick mark.
this.labelPosition = 'auto';
this.label = '';
this.value = null;
this._styles = {};
// prop: formatter
// A class of a formatter for the tick text.
// The default $.jqplot.DefaultTickFormatter uses sprintf.
this.formatter = $.jqplot.DefaultTickFormatter;
// prop: formatString
// string passed to the formatter.
this.formatString = '';
// prop: prefix
// String to prepend to the tick label.
// Prefix is prepended to the formatted tick label.
this.prefix = '';
// prop: fontFamily
// css spec for the font-family css attribute.
this.fontFamily = '"Trebuchet MS", Arial, Helvetica, sans-serif';
// prop: fontSize
// CSS spec for font size.
this.fontSize = '10pt';
// prop: fontWeight
// CSS spec for fontWeight
this.fontWeight = 'normal';
// prop: fontStretch
// Multiplier to condense or expand font width.
// Applies only to browsers which don't support canvas native font rendering.
this.fontStretch = 1.0;
// prop: textColor
// css spec for the color attribute.
this.textColor = '#666666';
// prop: enableFontSupport
// true to turn on native canvas font support in Mozilla 3.5+ and Safari 4+.
// If true, tick label will be drawn with canvas tag native support for fonts.
// If false, tick label will be drawn with Hershey font metrics.
this.enableFontSupport = true;
// prop: pt2px
// Point to pixel scaling factor, used for computing height of bounding box
// around a label. The labels text renderer has a default setting of 1.4, which
// should be suitable for most fonts. Leave as null to use default. If tops of
// letters appear clipped, increase this. If bounding box seems too big, decrease.
// This is an issue only with the native font renderering capabilities of Mozilla
// 3.5 and Safari 4 since they do not provide a method to determine the font height.
this.pt2px = null;
this._elem;
this._ctx;
this._plotWidth;
this._plotHeight;
this._plotDimensions = {height:null, width:null};
$.extend(true, this, options);
var ropts = {fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily};
if (this.pt2px) {
ropts.pt2px = this.pt2px;
}
if (this.enableFontSupport) {
if ($.jqplot.support_canvas_text()) {
this._textRenderer = new $.jqplot.CanvasFontRenderer(ropts);
}
else {
this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
}
}
else {
this._textRenderer = new $.jqplot.CanvasTextRenderer(ropts);
}
};
$.jqplot.CanvasAxisTickRenderer.prototype.init = function(options) {
$.extend(true, this, options);
this._textRenderer.init({fontSize:this.fontSize, fontWeight:this.fontWeight, fontStretch:this.fontStretch, fillStyle:this.textColor, angle:this.getAngleRad(), fontFamily:this.fontFamily});
};
// return width along the x axis
// will check first to see if an element exists.
// if not, will return the computed text box width.
$.jqplot.CanvasAxisTickRenderer.prototype.getWidth = function(ctx) {
if (this._elem) {
return this._elem.outerWidth(true);
}
else {
var tr = this._textRenderer;
var l = tr.getWidth(ctx);
var h = tr.getHeight(ctx);
var w = Math.abs(Math.sin(tr.angle)*h) + Math.abs(Math.cos(tr.angle)*l);
return w;
}
};
// return height along the y axis.
$.jqplot.CanvasAxisTickRenderer.prototype.getHeight = function(ctx) {
if (this._elem) {
return this._elem.outerHeight(true);
}
else {
var tr = this._textRenderer;
var l = tr.getWidth(ctx);
var h = tr.getHeight(ctx);
var w = Math.abs(Math.cos(tr.angle)*h) + Math.abs(Math.sin(tr.angle)*l);
return w;
}
};
// return top.
$.jqplot.CanvasAxisTickRenderer.prototype.getTop = function(ctx) {
if (this._elem) {
return this._elem.position().top;
}
else {
return null;
}
};
$.jqplot.CanvasAxisTickRenderer.prototype.getAngleRad = function() {
var a = this.angle * Math.PI/180;
return a;
};
$.jqplot.CanvasAxisTickRenderer.prototype.setTick = function(value, axisName, isMinor) {
this.value = value;
if (isMinor) {
this.isMinorTick = true;
}
return this;
};
$.jqplot.CanvasAxisTickRenderer.prototype.draw = function(ctx, plot) {
if (!this.label) {
this.label = this.prefix + this.formatter(this.formatString, this.value);
}
// Memory Leaks patch
if (this._elem) {
if ($.jqplot.use_excanvas && window.G_vmlCanvasManager.uninitElement !== undefined) {
window.G_vmlCanvasManager.uninitElement(this._elem.get(0));
}
this._elem.emptyForce();
this._elem = null;
}
// create a canvas here, but can't draw on it untill it is appended
// to dom for IE compatability.
var elem = plot.canvasManager.getCanvas();
this._textRenderer.setText(this.label, ctx);
var w = this.getWidth(ctx);
var h = this.getHeight(ctx);
// canvases seem to need to have width and heigh attributes directly set.
elem.width = w;
elem.height = h;
elem.style.width = w;
elem.style.height = h;
elem.style.textAlign = 'left';
elem.style.position = 'absolute';
elem = plot.canvasManager.initCanvas(elem);
this._elem = $(elem);
this._elem.css(this._styles);
this._elem.addClass('jqplot-'+this.axis+'-tick');
elem = null;
return this._elem;
};
$.jqplot.CanvasAxisTickRenderer.prototype.pack = function() {
this._textRenderer.draw(this._elem.get(0).getContext("2d"), this.label);
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,449 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
* included jsDate library by Chris Leonello:
*
* Copyright (c) 2010-2015 Chris Leonello
*
* jsDate is currently available for use in all personal or commercial projects
* under both the MIT and GPL version 2.0 licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* jsDate borrows many concepts and ideas from the Date Instance
* Methods by Ken Snyder along with some parts of Ken's actual code.
*
* Ken's original Date Instance Methods and copyright notice:
*
* Ken Snyder (ken d snyder at gmail dot com)
* 2008-09-10
* version 2.0.2 (http://kendsnyder.com/sandbox/date/)
* Creative Commons Attribution License 3.0 (http://creativecommons.org/licenses/by/3.0/)
*
* jqplotToImage function based on Larry Siden's export-jqplot-to-png.js.
* Larry has generously given permission to adapt his code for inclusion
* into jqPlot.
*
* Larry's original code can be found here:
*
* https://github.com/lsiden/export-jqplot-to-png
*
*
*/
(function($) {
// This code is a modified version of the canvastext.js code, copyright below:
//
// This code is released to the public domain by Jim Studt, 2007.
// He may keep some sort of up to date copy at http://www.federated.com/~jim/canvastext/
//
$.jqplot.CanvasTextRenderer = function(options){
this.fontStyle = 'normal'; // normal, italic, oblique [not implemented]
this.fontVariant = 'normal'; // normal, small caps [not implemented]
this.fontWeight = 'normal'; // normal, bold, bolder, lighter, 100 - 900
this.fontSize = '10px';
this.fontFamily = 'sans-serif';
this.fontStretch = 1.0;
this.fillStyle = '#666666';
this.angle = 0;
this.textAlign = 'start';
this.textBaseline = 'alphabetic';
this.text;
this.width;
this.height;
this.pt2px = 1.28;
$.extend(true, this, options);
this.normalizedFontSize = this.normalizeFontSize(this.fontSize);
this.setHeight();
};
$.jqplot.CanvasTextRenderer.prototype.init = function(options) {
$.extend(true, this, options);
this.normalizedFontSize = this.normalizeFontSize(this.fontSize);
this.setHeight();
};
// convert css spec into point size
// returns float
$.jqplot.CanvasTextRenderer.prototype.normalizeFontSize = function(sz) {
sz = String(sz);
var n = parseFloat(sz);
if (sz.indexOf('px') > -1) {
return n/this.pt2px;
}
else if (sz.indexOf('pt') > -1) {
return n;
}
else if (sz.indexOf('em') > -1) {
return n*12;
}
else if (sz.indexOf('%') > -1) {
return n*12/100;
}
// default to pixels;
else {
return n/this.pt2px;
}
};
$.jqplot.CanvasTextRenderer.prototype.fontWeight2Float = function(w) {
// w = normal | bold | bolder | lighter | 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900
// return values adjusted for Hershey font.
if (Number(w)) {
return w/400;
}
else {
switch (w) {
case 'normal':
return 1;
break;
case 'bold':
return 1.75;
break;
case 'bolder':
return 2.25;
break;
case 'lighter':
return 0.75;
break;
default:
return 1;
break;
}
}
};
$.jqplot.CanvasTextRenderer.prototype.getText = function() {
return this.text;
};
$.jqplot.CanvasTextRenderer.prototype.setText = function(t, ctx) {
this.text = t;
this.setWidth(ctx);
return this;
};
$.jqplot.CanvasTextRenderer.prototype.getWidth = function(ctx) {
return this.width;
};
$.jqplot.CanvasTextRenderer.prototype.setWidth = function(ctx, w) {
if (!w) {
this.width = this.measure(ctx, this.text);
}
else {
this.width = w;
}
return this;
};
// return height in pixels.
$.jqplot.CanvasTextRenderer.prototype.getHeight = function(ctx) {
return this.height;
};
// w - height in pt
// set heigh in px
$.jqplot.CanvasTextRenderer.prototype.setHeight = function(w) {
if (!w) {
//height = this.fontSize /0.75;
this.height = this.normalizedFontSize * this.pt2px;
}
else {
this.height = w;
}
return this;
};
$.jqplot.CanvasTextRenderer.prototype.letter = function (ch)
{
return this.letters[ch];
};
$.jqplot.CanvasTextRenderer.prototype.ascent = function()
{
return this.normalizedFontSize;
};
$.jqplot.CanvasTextRenderer.prototype.descent = function()
{
return 7.0*this.normalizedFontSize/25.0;
};
$.jqplot.CanvasTextRenderer.prototype.measure = function(ctx, str)
{
var total = 0;
var len = str.length;
for (var i = 0; i < len; i++) {
var c = this.letter(str.charAt(i));
if (c) {
total += c.width * this.normalizedFontSize / 25.0 * this.fontStretch;
}
}
return total;
};
$.jqplot.CanvasTextRenderer.prototype.draw = function(ctx,str)
{
var x = 0;
// leave room at bottom for descenders.
var y = this.height*0.72;
var total = 0;
var len = str.length;
var mag = this.normalizedFontSize / 25.0;
ctx.save();
var tx, ty;
// 1st quadrant
if ((-Math.PI/2 <= this.angle && this.angle <= 0) || (Math.PI*3/2 <= this.angle && this.angle <= Math.PI*2)) {
tx = 0;
ty = -Math.sin(this.angle) * this.width;
}
// 4th quadrant
else if ((0 < this.angle && this.angle <= Math.PI/2) || (-Math.PI*2 <= this.angle && this.angle <= -Math.PI*3/2)) {
tx = Math.sin(this.angle) * this.height;
ty = 0;
}
// 2nd quadrant
else if ((-Math.PI < this.angle && this.angle < -Math.PI/2) || (Math.PI <= this.angle && this.angle <= Math.PI*3/2)) {
tx = -Math.cos(this.angle) * this.width;
ty = -Math.sin(this.angle) * this.width - Math.cos(this.angle) * this.height;
}
// 3rd quadrant
else if ((-Math.PI*3/2 < this.angle && this.angle < Math.PI) || (Math.PI/2 < this.angle && this.angle < Math.PI)) {
tx = Math.sin(this.angle) * this.height - Math.cos(this.angle)*this.width;
ty = -Math.cos(this.angle) * this.height;
}
ctx.strokeStyle = this.fillStyle;
ctx.fillStyle = this.fillStyle;
ctx.translate(tx, ty);
ctx.rotate(this.angle);
ctx.lineCap = "round";
// multiplier was 2.0
var fact = (this.normalizedFontSize > 30) ? 2.0 : 2 + (30 - this.normalizedFontSize)/20;
ctx.lineWidth = fact * mag * this.fontWeight2Float(this.fontWeight);
for ( var i = 0; i < len; i++) {
var c = this.letter( str.charAt(i));
if ( !c) {
continue;
}
ctx.beginPath();
var penUp = 1;
var needStroke = 0;
for ( var j = 0; j < c.points.length; j++) {
var a = c.points[j];
if ( a[0] == -1 && a[1] == -1) {
penUp = 1;
continue;
}
if ( penUp) {
ctx.moveTo( x + a[0]*mag*this.fontStretch, y - a[1]*mag);
penUp = false;
} else {
ctx.lineTo( x + a[0]*mag*this.fontStretch, y - a[1]*mag);
}
}
ctx.stroke();
x += c.width*mag*this.fontStretch;
}
ctx.restore();
return total;
};
$.jqplot.CanvasTextRenderer.prototype.letters = {
' ': { width: 16, points: [] },
'!': { width: 10, points: [[5,21],[5,7],[-1,-1],[5,2],[4,1],[5,0],[6,1],[5,2]] },
'"': { width: 16, points: [[4,21],[4,14],[-1,-1],[12,21],[12,14]] },
'#': { width: 21, points: [[11,25],[4,-7],[-1,-1],[17,25],[10,-7],[-1,-1],[4,12],[18,12],[-1,-1],[3,6],[17,6]] },
'$': { width: 20, points: [[8,25],[8,-4],[-1,-1],[12,25],[12,-4],[-1,-1],[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },
'%': { width: 24, points: [[21,21],[3,0],[-1,-1],[8,21],[10,19],[10,17],[9,15],[7,14],[5,14],[3,16],[3,18],[4,20],[6,21],[8,21],[10,20],[13,19],[16,19],[19,20],[21,21],[-1,-1],[17,7],[15,6],[14,4],[14,2],[16,0],[18,0],[20,1],[21,3],[21,5],[19,7],[17,7]] },
'&': { width: 26, points: [[23,12],[23,13],[22,14],[21,14],[20,13],[19,11],[17,6],[15,3],[13,1],[11,0],[7,0],[5,1],[4,2],[3,4],[3,6],[4,8],[5,9],[12,13],[13,14],[14,16],[14,18],[13,20],[11,21],[9,20],[8,18],[8,16],[9,13],[11,10],[16,3],[18,1],[20,0],[22,0],[23,1],[23,2]] },
'\'': { width: 10, points: [[5,19],[4,20],[5,21],[6,20],[6,18],[5,16],[4,15]] },
'(': { width: 14, points: [[11,25],[9,23],[7,20],[5,16],[4,11],[4,7],[5,2],[7,-2],[9,-5],[11,-7]] },
')': { width: 14, points: [[3,25],[5,23],[7,20],[9,16],[10,11],[10,7],[9,2],[7,-2],[5,-5],[3,-7]] },
'*': { width: 16, points: [[8,21],[8,9],[-1,-1],[3,18],[13,12],[-1,-1],[13,18],[3,12]] },
'+': { width: 26, points: [[13,18],[13,0],[-1,-1],[4,9],[22,9]] },
',': { width: 10, points: [[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },
'-': { width: 18, points: [[6,9],[12,9]] },
'.': { width: 10, points: [[5,2],[4,1],[5,0],[6,1],[5,2]] },
'/': { width: 22, points: [[20,25],[2,-7]] },
'0': { width: 20, points: [[9,21],[6,20],[4,17],[3,12],[3,9],[4,4],[6,1],[9,0],[11,0],[14,1],[16,4],[17,9],[17,12],[16,17],[14,20],[11,21],[9,21]] },
'1': { width: 20, points: [[6,17],[8,18],[11,21],[11,0]] },
'2': { width: 20, points: [[4,16],[4,17],[5,19],[6,20],[8,21],[12,21],[14,20],[15,19],[16,17],[16,15],[15,13],[13,10],[3,0],[17,0]] },
'3': { width: 20, points: [[5,21],[16,21],[10,13],[13,13],[15,12],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },
'4': { width: 20, points: [[13,21],[3,7],[18,7],[-1,-1],[13,21],[13,0]] },
'5': { width: 20, points: [[15,21],[5,21],[4,12],[5,13],[8,14],[11,14],[14,13],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },
'6': { width: 20, points: [[16,18],[15,20],[12,21],[10,21],[7,20],[5,17],[4,12],[4,7],[5,3],[7,1],[10,0],[11,0],[14,1],[16,3],[17,6],[17,7],[16,10],[14,12],[11,13],[10,13],[7,12],[5,10],[4,7]] },
'7': { width: 20, points: [[17,21],[7,0],[-1,-1],[3,21],[17,21]] },
'8': { width: 20, points: [[8,21],[5,20],[4,18],[4,16],[5,14],[7,13],[11,12],[14,11],[16,9],[17,7],[17,4],[16,2],[15,1],[12,0],[8,0],[5,1],[4,2],[3,4],[3,7],[4,9],[6,11],[9,12],[13,13],[15,14],[16,16],[16,18],[15,20],[12,21],[8,21]] },
'9': { width: 20, points: [[16,14],[15,11],[13,9],[10,8],[9,8],[6,9],[4,11],[3,14],[3,15],[4,18],[6,20],[9,21],[10,21],[13,20],[15,18],[16,14],[16,9],[15,4],[13,1],[10,0],[8,0],[5,1],[4,3]] },
':': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],[-1,-1],[5,2],[4,1],[5,0],[6,1],[5,2]] },
';': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],[-1,-1],[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },
'<': { width: 24, points: [[20,18],[4,9],[20,0]] },
'=': { width: 26, points: [[4,12],[22,12],[-1,-1],[4,6],[22,6]] },
'>': { width: 24, points: [[4,18],[20,9],[4,0]] },
'?': { width: 18, points: [[3,16],[3,17],[4,19],[5,20],[7,21],[11,21],[13,20],[14,19],[15,17],[15,15],[14,13],[13,12],[9,10],[9,7],[-1,-1],[9,2],[8,1],[9,0],[10,1],[9,2]] },
'@': { width: 27, points: [[18,13],[17,15],[15,16],[12,16],[10,15],[9,14],[8,11],[8,8],[9,6],[11,5],[14,5],[16,6],[17,8],[-1,-1],[12,16],[10,14],[9,11],[9,8],[10,6],[11,5],[-1,-1],[18,16],[17,8],[17,6],[19,5],[21,5],[23,7],[24,10],[24,12],[23,15],[22,17],[20,19],[18,20],[15,21],[12,21],[9,20],[7,19],[5,17],[4,15],[3,12],[3,9],[4,6],[5,4],[7,2],[9,1],[12,0],[15,0],[18,1],[20,2],[21,3],[-1,-1],[19,16],[18,8],[18,6],[19,5]] },
'A': { width: 18, points: [[9,21],[1,0],[-1,-1],[9,21],[17,0],[-1,-1],[4,7],[14,7]] },
'B': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[-1,-1],[4,11],[13,11],[16,10],[17,9],[18,7],[18,4],[17,2],[16,1],[13,0],[4,0]] },
'C': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5]] },
'D': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[11,21],[14,20],[16,18],[17,16],[18,13],[18,8],[17,5],[16,3],[14,1],[11,0],[4,0]] },
'E': { width: 19, points: [[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11],[-1,-1],[4,0],[17,0]] },
'F': { width: 18, points: [[4,21],[4,0],[-1,-1],[4,21],[17,21],[-1,-1],[4,11],[12,11]] },
'G': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[18,8],[-1,-1],[13,8],[18,8]] },
'H': { width: 22, points: [[4,21],[4,0],[-1,-1],[18,21],[18,0],[-1,-1],[4,11],[18,11]] },
'I': { width: 8, points: [[4,21],[4,0]] },
'J': { width: 16, points: [[12,21],[12,5],[11,2],[10,1],[8,0],[6,0],[4,1],[3,2],[2,5],[2,7]] },
'K': { width: 21, points: [[4,21],[4,0],[-1,-1],[18,21],[4,7],[-1,-1],[9,12],[18,0]] },
'L': { width: 17, points: [[4,21],[4,0],[-1,-1],[4,0],[16,0]] },
'M': { width: 24, points: [[4,21],[4,0],[-1,-1],[4,21],[12,0],[-1,-1],[20,21],[12,0],[-1,-1],[20,21],[20,0]] },
'N': { width: 22, points: [[4,21],[4,0],[-1,-1],[4,21],[18,0],[-1,-1],[18,21],[18,0]] },
'O': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21]] },
'P': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,14],[17,12],[16,11],[13,10],[4,10]] },
'Q': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21],[-1,-1],[12,4],[18,-2]] },
'R': { width: 21, points: [[4,21],[4,0],[-1,-1],[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[4,11],[-1,-1],[11,11],[18,0]] },
'S': { width: 20, points: [[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },
'T': { width: 16, points: [[8,21],[8,0],[-1,-1],[1,21],[15,21]] },
'U': { width: 22, points: [[4,21],[4,6],[5,3],[7,1],[10,0],[12,0],[15,1],[17,3],[18,6],[18,21]] },
'V': { width: 18, points: [[1,21],[9,0],[-1,-1],[17,21],[9,0]] },
'W': { width: 24, points: [[2,21],[7,0],[-1,-1],[12,21],[7,0],[-1,-1],[12,21],[17,0],[-1,-1],[22,21],[17,0]] },
'X': { width: 20, points: [[3,21],[17,0],[-1,-1],[17,21],[3,0]] },
'Y': { width: 18, points: [[1,21],[9,11],[9,0],[-1,-1],[17,21],[9,11]] },
'Z': { width: 20, points: [[17,21],[3,0],[-1,-1],[3,21],[17,21],[-1,-1],[3,0],[17,0]] },
'[': { width: 14, points: [[4,25],[4,-7],[-1,-1],[5,25],[5,-7],[-1,-1],[4,25],[11,25],[-1,-1],[4,-7],[11,-7]] },
'\\': { width: 14, points: [[0,21],[14,-3]] },
']': { width: 14, points: [[9,25],[9,-7],[-1,-1],[10,25],[10,-7],[-1,-1],[3,25],[10,25],[-1,-1],[3,-7],[10,-7]] },
'^': { width: 16, points: [[6,15],[8,18],[10,15],[-1,-1],[3,12],[8,17],[13,12],[-1,-1],[8,17],[8,0]] },
'_': { width: 16, points: [[0,-2],[16,-2]] },
'`': { width: 10, points: [[6,21],[5,20],[4,18],[4,16],[5,15],[6,16],[5,17]] },
'a': { width: 19, points: [[15,14],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
'b': { width: 19, points: [[4,21],[4,0],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },
'c': { width: 18, points: [[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
'd': { width: 19, points: [[15,21],[15,0],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
'e': { width: 18, points: [[3,8],[15,8],[15,10],[14,12],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
'f': { width: 12, points: [[10,21],[8,21],[6,20],[5,17],[5,0],[-1,-1],[2,14],[9,14]] },
'g': { width: 19, points: [[15,14],[15,-2],[14,-5],[13,-6],[11,-7],[8,-7],[6,-6],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
'h': { width: 19, points: [[4,21],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },
'i': { width: 8, points: [[3,21],[4,20],[5,21],[4,22],[3,21],[-1,-1],[4,14],[4,0]] },
'j': { width: 10, points: [[5,21],[6,20],[7,21],[6,22],[5,21],[-1,-1],[6,14],[6,-3],[5,-6],[3,-7],[1,-7]] },
'k': { width: 17, points: [[4,21],[4,0],[-1,-1],[14,14],[4,4],[-1,-1],[8,8],[15,0]] },
'l': { width: 8, points: [[4,21],[4,0]] },
'm': { width: 30, points: [[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0],[-1,-1],[15,10],[18,13],[20,14],[23,14],[25,13],[26,10],[26,0]] },
'n': { width: 19, points: [[4,14],[4,0],[-1,-1],[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },
'o': { width: 19, points: [[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3],[16,6],[16,8],[15,11],[13,13],[11,14],[8,14]] },
'p': { width: 19, points: [[4,14],[4,-7],[-1,-1],[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },
'q': { width: 19, points: [[15,14],[15,-7],[-1,-1],[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },
'r': { width: 13, points: [[4,14],[4,0],[-1,-1],[4,8],[5,11],[7,13],[9,14],[12,14]] },
's': { width: 17, points: [[14,11],[13,13],[10,14],[7,14],[4,13],[3,11],[4,9],[6,8],[11,7],[13,6],[14,4],[14,3],[13,1],[10,0],[7,0],[4,1],[3,3]] },
't': { width: 12, points: [[5,21],[5,4],[6,1],[8,0],[10,0],[-1,-1],[2,14],[9,14]] },
'u': { width: 19, points: [[4,14],[4,4],[5,1],[7,0],[10,0],[12,1],[15,4],[-1,-1],[15,14],[15,0]] },
'v': { width: 16, points: [[2,14],[8,0],[-1,-1],[14,14],[8,0]] },
'w': { width: 22, points: [[3,14],[7,0],[-1,-1],[11,14],[7,0],[-1,-1],[11,14],[15,0],[-1,-1],[19,14],[15,0]] },
'x': { width: 17, points: [[3,14],[14,0],[-1,-1],[14,14],[3,0]] },
'y': { width: 16, points: [[2,14],[8,0],[-1,-1],[14,14],[8,0],[6,-4],[4,-6],[2,-7],[1,-7]] },
'z': { width: 17, points: [[14,14],[3,0],[-1,-1],[3,14],[14,14],[-1,-1],[3,0],[14,0]] },
'{': { width: 14, points: [[9,25],[7,24],[6,23],[5,21],[5,19],[6,17],[7,16],[8,14],[8,12],[6,10],[-1,-1],[7,24],[6,22],[6,20],[7,18],[8,17],[9,15],[9,13],[8,11],[4,9],[8,7],[9,5],[9,3],[8,1],[7,0],[6,-2],[6,-4],[7,-6],[-1,-1],[6,8],[8,6],[8,4],[7,2],[6,1],[5,-1],[5,-3],[6,-5],[7,-6],[9,-7]] },
'|': { width: 8, points: [[4,25],[4,-7]] },
'}': { width: 14, points: [[5,25],[7,24],[8,23],[9,21],[9,19],[8,17],[7,16],[6,14],[6,12],[8,10],[-1,-1],[7,24],[8,22],[8,20],[7,18],[6,17],[5,15],[5,13],[6,11],[10,9],[6,7],[5,5],[5,3],[6,1],[7,0],[8,-2],[8,-4],[7,-6],[-1,-1],[8,8],[6,6],[6,4],[7,2],[8,1],[9,-1],[9,-3],[8,-5],[7,-6],[5,-7]] },
'~': { width: 24, points: [[3,6],[3,8],[4,11],[6,12],[8,12],[10,11],[14,8],[16,7],[18,7],[20,8],[21,10],[-1,-1],[3,8],[4,10],[6,11],[8,11],[10,10],[14,7],[16,6],[18,6],[20,7],[21,10],[21,12]] }
};
$.jqplot.CanvasFontRenderer = function(options) {
options = options || {};
if (!options.pt2px) {
options.pt2px = 1.5;
}
$.jqplot.CanvasTextRenderer.call(this, options);
};
$.jqplot.CanvasFontRenderer.prototype = new $.jqplot.CanvasTextRenderer({});
$.jqplot.CanvasFontRenderer.prototype.constructor = $.jqplot.CanvasFontRenderer;
$.jqplot.CanvasFontRenderer.prototype.measure = function(ctx, str)
{
// var fstyle = this.fontStyle+' '+this.fontVariant+' '+this.fontWeight+' '+this.fontSize+' '+this.fontFamily;
var fstyle = this.fontSize+' '+this.fontFamily;
ctx.save();
ctx.font = fstyle;
var w = ctx.measureText(str).width;
ctx.restore();
return w;
};
$.jqplot.CanvasFontRenderer.prototype.draw = function(ctx, str)
{
var x = 0;
// leave room at bottom for descenders.
var y = this.height*0.72;
//var y = 12;
ctx.save();
var tx, ty;
// 1st quadrant
if ((-Math.PI/2 <= this.angle && this.angle <= 0) || (Math.PI*3/2 <= this.angle && this.angle <= Math.PI*2)) {
tx = 0;
ty = -Math.sin(this.angle) * this.width;
}
// 4th quadrant
else if ((0 < this.angle && this.angle <= Math.PI/2) || (-Math.PI*2 <= this.angle && this.angle <= -Math.PI*3/2)) {
tx = Math.sin(this.angle) * this.height;
ty = 0;
}
// 2nd quadrant
else if ((-Math.PI < this.angle && this.angle < -Math.PI/2) || (Math.PI <= this.angle && this.angle <= Math.PI*3/2)) {
tx = -Math.cos(this.angle) * this.width;
ty = -Math.sin(this.angle) * this.width - Math.cos(this.angle) * this.height;
}
// 3rd quadrant
else if ((-Math.PI*3/2 < this.angle && this.angle < Math.PI) || (Math.PI/2 < this.angle && this.angle < Math.PI)) {
tx = Math.sin(this.angle) * this.height - Math.cos(this.angle)*this.width;
ty = -Math.cos(this.angle) * this.height;
}
ctx.strokeStyle = this.fillStyle;
ctx.fillStyle = this.fillStyle;
// var fstyle = this.fontStyle+' '+this.fontVariant+' '+this.fontWeight+' '+this.fontSize+' '+this.fontFamily;
var fstyle = this.fontSize+' '+this.fontFamily;
ctx.font = fstyle;
ctx.translate(tx, ty);
ctx.rotate(this.angle);
ctx.fillText(str, x, y);
// ctx.strokeText(str, x, y);
ctx.restore();
};
})(jQuery);

View File

@@ -0,0 +1,679 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* class: $.jqplot.CategoryAxisRenderer
* A plugin for jqPlot to render a category style axis, with equal pixel spacing between y data values of a series.
*
* To use this renderer, include the plugin in your source
* > <script type="text/javascript" language="javascript" src="plugins/jqplot.categoryAxisRenderer.js"></script>
*
* and supply the appropriate options to your plot
*
* > {axes:{xaxis:{renderer:$.jqplot.CategoryAxisRenderer}}}
**/
$.jqplot.CategoryAxisRenderer = function(options) {
$.jqplot.LinearAxisRenderer.call(this);
// prop: sortMergedLabels
// True to sort tick labels when labels are created by merging
// x axis values from multiple series. That is, say you have
// two series like:
// > line1 = [[2006, 4], [2008, 9], [2009, 16]];
// > line2 = [[2006, 3], [2007, 7], [2008, 6]];
// If no label array is specified, tick labels will be collected
// from the x values of the series. With sortMergedLabels
// set to true, tick labels will be:
// > [2006, 2007, 2008, 2009]
// With sortMergedLabels set to false, tick labels will be:
// > [2006, 2008, 2009, 2007]
//
// Note, this property is specified on the renderOptions for the
// axes when creating a plot:
// > axes:{xaxis:{renderer:$.jqplot.CategoryAxisRenderer, rendererOptions:{sortMergedLabels:true}}}
this.sortMergedLabels = false;
};
$.jqplot.CategoryAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.CategoryAxisRenderer.prototype.constructor = $.jqplot.CategoryAxisRenderer;
$.jqplot.CategoryAxisRenderer.prototype.init = function(options){
this.groups = 1;
this.groupLabels = [];
this._groupLabels = [];
this._grouped = false;
this._barsPerGroup = null;
this.reverse = false;
// prop: tickRenderer
// A class of a rendering engine for creating the ticks labels displayed on the plot,
// See <$.jqplot.AxisTickRenderer>.
// this.tickRenderer = $.jqplot.AxisTickRenderer;
// this.labelRenderer = $.jqplot.AxisLabelRenderer;
$.extend(true, this, {tickOptions:{formatString:'%d'}}, options);
var db = this._dataBounds;
// Go through all the series attached to this axis and find
// the min/max bounds for this axis.
for (var i=0; i<this._series.length; i++) {
var s = this._series[i];
if (s.groups) {
this.groups = s.groups;
}
var d = s.data;
for (var j=0; j<d.length; j++) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
if (d[j][0] < db.min || db.min == null) {
db.min = d[j][0];
}
if (d[j][0] > db.max || db.max == null) {
db.max = d[j][0];
}
}
else {
if (d[j][1] < db.min || db.min == null) {
db.min = d[j][1];
}
if (d[j][1] > db.max || db.max == null) {
db.max = d[j][1];
}
}
}
}
if (this.groupLabels.length) {
this.groups = this.groupLabels.length;
}
};
$.jqplot.CategoryAxisRenderer.prototype.createTicks = function() {
// we're are operating on an axis here
var ticks = this._ticks;
var userTicks = this.ticks;
var name = this.name;
// databounds were set on axis initialization.
var db = this._dataBounds;
var dim, interval;
var min, max;
var pos1, pos2;
var tt, i;
// if we already have ticks, use them.
if (userTicks.length) {
// adjust with blanks if we have groups
if (this.groups > 1 && !this._grouped) {
var l = userTicks.length;
var skip = parseInt(l/this.groups, 10);
var count = 0;
for (var i=skip; i<l; i+=skip) {
userTicks.splice(i+count, 0, ' ');
count++;
}
this._grouped = true;
}
this.min = 0.5;
this.max = userTicks.length + 0.5;
var range = this.max - this.min;
this.numberTicks = 2*userTicks.length + 1;
for (i=0; i<userTicks.length; i++){
tt = this.min + 2 * i * range / (this.numberTicks-1);
// need a marker before and after the tick
var t = new this.tickRenderer(this.tickOptions);
t.showLabel = false;
// t.showMark = true;
t.setTick(tt, this.name);
this._ticks.push(t);
var t = new this.tickRenderer(this.tickOptions);
t.label = userTicks[i];
// t.showLabel = true;
t.showMark = false;
t.showGridline = false;
t.setTick(tt+0.5, this.name);
this._ticks.push(t);
}
// now add the last tick at the end
var t = new this.tickRenderer(this.tickOptions);
t.showLabel = false;
// t.showMark = true;
t.setTick(tt+1, this.name);
this._ticks.push(t);
}
// we don't have any ticks yet, let's make some!
else {
if (name == 'xaxis' || name == 'x2axis') {
dim = this._plotDimensions.width;
}
else {
dim = this._plotDimensions.height;
}
// if min, max and number of ticks specified, user can't specify interval.
if (this.min != null && this.max != null && this.numberTicks != null) {
this.tickInterval = null;
}
// if max, min, and interval specified and interval won't fit, ignore interval.
if (this.min != null && this.max != null && this.tickInterval != null) {
if (parseInt((this.max-this.min)/this.tickInterval, 10) != (this.max-this.min)/this.tickInterval) {
this.tickInterval = null;
}
}
// find out how many categories are in the lines and collect labels
var labels = [];
var numcats = 0;
var min = 0.5;
var max, val;
var isMerged = false;
for (var i=0; i<this._series.length; i++) {
var s = this._series[i];
for (var j=0; j<s.data.length; j++) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
val = s.data[j][0];
}
else {
val = s.data[j][1];
}
if ($.inArray(val, labels) == -1) {
isMerged = true;
numcats += 1;
labels.push(val);
}
}
}
if (isMerged && this.sortMergedLabels) {
if (typeof labels[0] == "string") {
labels.sort();
} else {
labels.sort(function(a,b) { return a - b; });
}
}
// keep a reference to these tick labels to use for redrawing plot (see bug #57)
this.ticks = labels;
// now bin the data values to the right lables.
for (var i=0; i<this._series.length; i++) {
var s = this._series[i];
for (var j=0; j<s.data.length; j++) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
val = s.data[j][0];
}
else {
val = s.data[j][1];
}
// for category axis, force the values into category bins.
// we should have the value in the label array now.
var idx = $.inArray(val, labels)+1;
if (this.name == 'xaxis' || this.name == 'x2axis') {
s.data[j][0] = idx;
}
else {
s.data[j][1] = idx;
}
}
}
// adjust with blanks if we have groups
if (this.groups > 1 && !this._grouped) {
var l = labels.length;
var skip = parseInt(l/this.groups, 10);
var count = 0;
for (var i=skip; i<l; i+=skip+1) {
labels[i] = ' ';
}
this._grouped = true;
}
max = numcats + 0.5;
if (this.numberTicks == null) {
this.numberTicks = 2*numcats + 1;
}
var range = max - min;
this.min = min;
this.max = max;
var track = 0;
// todo: adjust this so more ticks displayed.
var maxVisibleTicks = parseInt(3+dim/10, 10);
var skip = parseInt(numcats/maxVisibleTicks, 10);
if (this.tickInterval == null) {
this.tickInterval = range / (this.numberTicks-1);
}
// if tickInterval is specified, we will ignore any computed maximum.
for (var i=0; i<this.numberTicks; i++){
tt = this.min + i * this.tickInterval;
var t = new this.tickRenderer(this.tickOptions);
// if even tick, it isn't a category, it's a divider
if (i/2 == parseInt(i/2, 10)) {
t.showLabel = false;
t.showMark = true;
}
else {
if (skip>0 && track<skip) {
t.showLabel = false;
track += 1;
}
else {
t.showLabel = true;
track = 0;
}
t.label = t.formatter(t.formatString, labels[(i-1)/2]);
t.showMark = false;
t.showGridline = false;
}
t.setTick(tt, this.name);
this._ticks.push(t);
}
}
};
// called with scope of axis
$.jqplot.CategoryAxisRenderer.prototype.draw = function(ctx, plot) {
if (this.show) {
// populate the axis label and value properties.
// createTicks is a method on the renderer, but
// call it within the scope of the axis.
this.renderer.createTicks.call(this);
// fill a div with axes labels in the right direction.
// Need to pregenerate each axis to get its bounds and
// position it and the labels correctly on the plot.
var dim=0;
var temp;
// Added for theming.
if (this._elem) {
// this._elem.empty();
// Memory Leaks patch
this._elem.emptyForce();
}
this._elem = this._elem || $('<div class="jqplot-axis jqplot-'+this.name+'" style="position:absolute;"></div>');
if (this.name == 'xaxis' || this.name == 'x2axis') {
this._elem.width(this._plotDimensions.width);
}
else {
this._elem.height(this._plotDimensions.height);
}
// create a _label object.
this.labelOptions.axis = this.name;
this._label = new this.labelRenderer(this.labelOptions);
if (this._label.show) {
var elem = this._label.draw(ctx, plot);
elem.appendTo(this._elem);
}
var t = this._ticks;
for (var i=0; i<t.length; i++) {
var tick = t[i];
if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
var elem = tick.draw(ctx, plot);
elem.appendTo(this._elem);
}
}
this._groupLabels = [];
// now make group labels
for (var i=0; i<this.groupLabels.length; i++)
{
var elem = $('<div style="position:absolute;" class="jqplot-'+this.name+'-groupLabel"></div>');
elem.html(this.groupLabels[i]);
this._groupLabels.push(elem);
elem.appendTo(this._elem);
}
}
return this._elem;
};
// called with scope of axis
$.jqplot.CategoryAxisRenderer.prototype.set = function() {
var dim = 0;
var temp;
var w = 0;
var h = 0;
var lshow = (this._label == null) ? false : this._label.show;
if (this.show) {
var t = this._ticks;
for (var i=0; i<t.length; i++) {
var tick = t[i];
if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
temp = tick._elem.outerHeight(true);
}
else {
temp = tick._elem.outerWidth(true);
}
if (temp > dim) {
dim = temp;
}
}
}
var dim2 = 0;
for (var i=0; i<this._groupLabels.length; i++) {
var l = this._groupLabels[i];
if (this.name == 'xaxis' || this.name == 'x2axis') {
temp = l.outerHeight(true);
}
else {
temp = l.outerWidth(true);
}
if (temp > dim2) {
dim2 = temp;
}
}
if (lshow) {
w = this._label._elem.outerWidth(true);
h = this._label._elem.outerHeight(true);
}
if (this.name == 'xaxis') {
dim += dim2 + h;
this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
}
else if (this.name == 'x2axis') {
dim += dim2 + h;
this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
}
else if (this.name == 'yaxis') {
dim += dim2 + w;
this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css('width', w+'px');
}
}
else {
dim += dim2 + w;
this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css('width', w+'px');
}
}
}
};
// called with scope of axis
$.jqplot.CategoryAxisRenderer.prototype.pack = function(pos, offsets) {
var ticks = this._ticks;
var max = this.max;
var min = this.min;
var offmax = offsets.max;
var offmin = offsets.min;
var lshow = (this._label == null) ? false : this._label.show;
var i;
for (var p in pos) {
this._elem.css(p, pos[p]);
}
this._offsets = offsets;
// pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
var pixellength = offmax - offmin;
var unitlength = max - min;
if (!this.reverse) {
// point to unit and unit to point conversions references to Plot DOM element top left corner.
this.u2p = function(u){
return (u - min) * pixellength / unitlength + offmin;
};
this.p2u = function(p){
return (p - offmin) * unitlength / pixellength + min;
};
if (this.name == 'xaxis' || this.name == 'x2axis'){
this.series_u2p = function(u){
return (u - min) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + min;
};
}
else {
this.series_u2p = function(u){
return (u - max) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + max;
};
}
}
else {
// point to unit and unit to point conversions references to Plot DOM element top left corner.
this.u2p = function(u){
return offmin + (max - u) * pixellength / unitlength;
};
this.p2u = function(p){
return min + (p - offmin) * unitlength / pixellength;
};
if (this.name == 'xaxis' || this.name == 'x2axis'){
this.series_u2p = function(u){
return (max - u) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + max;
};
}
else {
this.series_u2p = function(u){
return (min - u) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + min;
};
}
}
if (this.show) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
for (i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
// will need to adjust auto positioning based on which axis this is.
var temp = (this.name == 'xaxis') ? 1 : -1;
switch (t.labelPosition) {
case 'auto':
// position at end
if (temp * t.angle < 0) {
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
}
// position at start
else {
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
}
break;
case 'end':
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
case 'start':
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
break;
case 'middle':
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
default:
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
}
}
else {
shim = -t.getWidth()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('left', val);
t.pack();
}
}
var labeledge=['bottom', 0];
if (lshow) {
var w = this._label._elem.outerWidth(true);
this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
if (this.name == 'xaxis') {
this._label._elem.css('bottom', '0px');
labeledge = ['bottom', this._label._elem.outerHeight(true)];
}
else {
this._label._elem.css('top', '0px');
labeledge = ['top', this._label._elem.outerHeight(true)];
}
this._label.pack();
}
// draw the group labels
var step = parseInt(this._ticks.length/this.groups, 10) + 1;
for (i=0; i<this._groupLabels.length; i++) {
var mid = 0;
var count = 0;
for (var j=i*step; j<(i+1)*step; j++) {
if (j >= this._ticks.length-1) continue; // the last tick does not exist as there is no other group in order to have an empty one.
if (this._ticks[j]._elem && this._ticks[j].label != " ") {
var t = this._ticks[j]._elem;
var p = t.position();
mid += p.left + t.outerWidth(true)/2;
count++;
}
}
mid = mid/count;
this._groupLabels[i].css({'left':(mid - this._groupLabels[i].outerWidth(true)/2)});
this._groupLabels[i].css(labeledge[0], labeledge[1]);
}
}
else {
for (i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
// will need to adjust auto positioning based on which axis this is.
var temp = (this.name == 'yaxis') ? 1 : -1;
switch (t.labelPosition) {
case 'auto':
// position at end
case 'end':
if (temp * t.angle < 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'start':
if (t.angle > 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'middle':
// if (t.angle > 0) {
// shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
// }
// else {
// shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
// }
shim = -t.getHeight()/2;
break;
default:
shim = -t.getHeight()/2;
break;
}
}
else {
shim = -t.getHeight()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('top', val);
t.pack();
}
}
var labeledge=['left', 0];
if (lshow) {
var h = this._label._elem.outerHeight(true);
this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
if (this.name == 'yaxis') {
this._label._elem.css('left', '0px');
labeledge = ['left', this._label._elem.outerWidth(true)];
}
else {
this._label._elem.css('right', '0px');
labeledge = ['right', this._label._elem.outerWidth(true)];
}
this._label.pack();
}
// draw the group labels, position top here, do left after label position.
var step = parseInt(this._ticks.length/this.groups, 10) + 1; // step is one more than before as we don't want to have overlaps in loops
for (i=0; i<this._groupLabels.length; i++) {
var mid = 0;
var count = 0;
for (var j=i*step; j<(i+1)*step; j++) { // j must never reach (i+1)*step as we don't want to have overlap between loops
if (j >= this._ticks.length-1) continue; // the last tick does not exist as there is no other group in order to have an empty one.
if (this._ticks[j]._elem && this._ticks[j].label != " ") {
var t = this._ticks[j]._elem;
var p = t.position();
mid += p.top + t.outerHeight()/2;
count++;
}
}
mid = mid/count;
this._groupLabels[i].css({'top':mid - this._groupLabels[i].outerHeight()/2});
this._groupLabels[i].css(labeledge[0], labeledge[1]);
}
}
}
};
})(jQuery);

View File

@@ -0,0 +1,116 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.ciParser
* Data Renderer function which converts a custom JSON data object into jqPlot data format.
* Set this as a callable on the jqplot dataRenderer plot option:
*
* > plot = $.jqplot('mychart', [data], { dataRenderer: $.jqplot.ciParser, ... });
*
* Where data is an object in JSON format or a JSON encoded string conforming to the
* City Index API spec.
*
* Note that calling the renderer function is handled internally by jqPlot. The
* user does not have to call the function. The parameters described below will
* automatically be passed to the ciParser function.
*
* Parameters:
* data - JSON encoded string or object.
* plot - reference to jqPlot Plot object.
*
* Returns:
* data array in jqPlot format.
*
*/
$.jqplot.ciParser = function (data, plot) {
var ret = [],
line,
temp,
i, j, k, kk;
if (typeof(data) == "string") {
data = $.jqplot.JSON.parse(data, handleStrings);
}
else if (typeof(data) == "object") {
for (k in data) {
for (i=0; i<data[k].length; i++) {
for (kk in data[k][i]) {
data[k][i][kk] = handleStrings(kk, data[k][i][kk]);
}
}
}
}
else {
return null;
}
// function handleStrings
// Checks any JSON encoded strings to see if they are
// encoded dates. If so, pull out the timestamp.
// Expects dates to be represented by js timestamps.
function handleStrings(key, value) {
var a;
if (value != null) {
if (value.toString().indexOf('Date') >= 0) {
//here we will try to extract the ticks from the Date string in the "value" fields of JSON returned data
a = /^\/Date\((-?[0-9]+)\)\/$/.exec(value);
if (a) {
return parseInt(a[1], 10);
}
}
return value;
}
}
for (var prop in data) {
line = [];
temp = data[prop];
switch (prop) {
case "PriceTicks":
for (i=0; i<temp.length; i++) {
line.push([temp[i]['TickDate'], temp[i]['Price']]);
}
break;
case "PriceBars":
for (i=0; i<temp.length; i++) {
line.push([temp[i]['BarDate'], temp[i]['Open'], temp[i]['High'], temp[i]['Low'], temp[i]['Close']]);
}
break;
}
ret.push(line);
}
return ret;
};
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,741 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.DateAxisRenderer
* A plugin for a jqPlot to render an axis as a series of date values.
* This renderer has no options beyond those supplied by the <Axis> class.
* It supplies its own tick formatter, so the tickOptions.formatter option
* should not be overridden.
*
* Thanks to Ken Synder for his enhanced Date instance methods which are
* included with this code <http://kendsnyder.com/sandbox/date/>.
*
* To use this renderer, include the plugin in your source
* > <script type="text/javascript" language="javascript" src="plugins/jqplot.dateAxisRenderer.js"></script>
*
* and supply the appropriate options to your plot
*
* > {axes:{xaxis:{renderer:$.jqplot.DateAxisRenderer}}}
*
* Dates can be passed into the axis in almost any recognizable value and
* will be parsed. They will be rendered on the axis in the format
* specified by tickOptions.formatString. e.g. tickOptions.formatString = '%Y-%m-%d'.
*
* Accecptable format codes
* are:
*
* > Code Result Description
* > == Years ==
* > %Y 2008 Four-digit year
* > %y 08 Two-digit year
* > == Months ==
* > %m 09 Two-digit month
* > %#m 9 One or two-digit month
* > %B September Full month name
* > %b Sep Abbreviated month name
* > == Days ==
* > %d 05 Two-digit day of month
* > %#d 5 One or two-digit day of month
* > %e 5 One or two-digit day of month
* > %A Sunday Full name of the day of the week
* > %a Sun Abbreviated name of the day of the week
* > %w 0 Number of the day of the week (0 = Sunday, 6 = Saturday)
* > %o th The ordinal suffix string following the day of the month
* > == Hours ==
* > %H 23 Hours in 24-hour format (two digits)
* > %#H 3 Hours in 24-hour integer format (one or two digits)
* > %I 11 Hours in 12-hour format (two digits)
* > %#I 3 Hours in 12-hour integer format (one or two digits)
* > %p PM AM or PM
* > == Minutes ==
* > %M 09 Minutes (two digits)
* > %#M 9 Minutes (one or two digits)
* > == Seconds ==
* > %S 02 Seconds (two digits)
* > %#S 2 Seconds (one or two digits)
* > %s 1206567625723 Unix timestamp (Seconds past 1970-01-01 00:00:00)
* > == Milliseconds ==
* > %N 008 Milliseconds (three digits)
* > %#N 8 Milliseconds (one to three digits)
* > == Timezone ==
* > %O 360 difference in minutes between local time and GMT
* > %Z Mountain Standard Time Name of timezone as reported by browser
* > %G -06:00 Hours and minutes between GMT
* > == Shortcuts ==
* > %F 2008-03-26 %Y-%m-%d
* > %T 05:06:30 %H:%M:%S
* > %X 05:06:30 %H:%M:%S
* > %x 03/26/08 %m/%d/%y
* > %D 03/26/08 %m/%d/%y
* > %#c Wed Mar 26 15:31:00 2008 %a %b %e %H:%M:%S %Y
* > %v 3-Sep-2008 %e-%b-%Y
* > %R 15:31 %H:%M
* > %r 3:31:00 PM %I:%M:%S %p
* > == Characters ==
* > %n \n Newline
* > %t \t Tab
* > %% % Percent Symbol
*/
$.jqplot.DateAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
this.date = new $.jsDate();
};
var second = 1000;
var minute = 60 * second;
var hour = 60 * minute;
var day = 24 * hour;
var week = 7 * day;
// these are less definitive
var month = 30.4368499 * day;
var year = 365.242199 * day;
var daysInMonths = [31,28,31,30,31,30,31,30,31,30,31,30];
// array of consistent nice intervals. Longer intervals
// will depend on days in month, days in year, etc.
var niceFormatStrings = ['%M:%S.%#N', '%M:%S.%#N', '%M:%S.%#N', '%M:%S', '%M:%S', '%M:%S', '%M:%S', '%H:%M:%S', '%H:%M:%S', '%H:%M', '%H:%M', '%H:%M', '%H:%M', '%H:%M', '%H:%M', '%a %H:%M', '%a %H:%M', '%b %e %H:%M', '%b %e %H:%M', '%b %e %H:%M', '%b %e %H:%M', '%v', '%v', '%v', '%v', '%v', '%v', '%v'];
var niceIntervals = [0.1*second, 0.2*second, 0.5*second, second, 2*second, 5*second, 10*second, 15*second, 30*second, minute, 2*minute, 5*minute, 10*minute, 15*minute, 30*minute, hour, 2*hour, 4*hour, 6*hour, 8*hour, 12*hour, day, 2*day, 3*day, 4*day, 5*day, week, 2*week];
var niceMonthlyIntervals = [];
function bestDateInterval(min, max, titarget) {
// iterate through niceIntervals to find one closest to titarget
var badness = Number.MAX_VALUE;
var temp, bestTi, bestfmt;
for (var i=0, l=niceIntervals.length; i < l; i++) {
temp = Math.abs(titarget - niceIntervals[i]);
if (temp < badness) {
badness = temp;
bestTi = niceIntervals[i];
bestfmt = niceFormatStrings[i];
}
}
return [bestTi, bestfmt];
}
$.jqplot.DateAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.DateAxisRenderer.prototype.constructor = $.jqplot.DateAxisRenderer;
$.jqplot.DateTickFormatter = function(format, val) {
if (!format) {
format = '%Y/%m/%d';
}
return $.jsDate.strftime(val, format);
};
$.jqplot.DateAxisRenderer.prototype.init = function(options){
// prop: tickRenderer
// A class of a rendering engine for creating the ticks labels displayed on the plot,
// See <$.jqplot.AxisTickRenderer>.
// this.tickRenderer = $.jqplot.AxisTickRenderer;
// this.labelRenderer = $.jqplot.AxisLabelRenderer;
this.tickOptions.formatter = $.jqplot.DateTickFormatter;
// prop: tickInset
// Controls the amount to inset the first and last ticks from
// the edges of the grid, in multiples of the tick interval.
// 0 is no inset, 0.5 is one half a tick interval, 1 is a full
// tick interval, etc.
this.tickInset = 0;
// prop: drawBaseline
// True to draw the axis baseline.
this.drawBaseline = true;
// prop: baselineWidth
// width of the baseline in pixels.
this.baselineWidth = null;
// prop: baselineColor
// CSS color spec for the baseline.
this.baselineColor = null;
this.daTickInterval = null;
this._daTickInterval = null;
$.extend(true, this, options);
var db = this._dataBounds,
stats,
sum,
s,
d,
pd,
sd,
intv;
// Go through all the series attached to this axis and find
// the min/max bounds for this axis.
for (var i=0; i<this._series.length; i++) {
stats = {intervals:[], frequencies:{}, sortedIntervals:[], min:null, max:null, mean:null};
sum = 0;
s = this._series[i];
d = s.data;
pd = s._plotData;
sd = s._stackData;
intv = 0;
for (var j=0; j<d.length; j++) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
d[j][0] = new $.jsDate(d[j][0]).getTime();
pd[j][0] = new $.jsDate(d[j][0]).getTime();
sd[j][0] = new $.jsDate(d[j][0]).getTime();
if ((d[j][0] != null && d[j][0] < db.min) || db.min == null) {
db.min = d[j][0];
}
if ((d[j][0] != null && d[j][0] > db.max) || db.max == null) {
db.max = d[j][0];
}
if (j>0) {
intv = Math.abs(d[j][0] - d[j-1][0]);
stats.intervals.push(intv);
if (stats.frequencies.hasOwnProperty(intv)) {
stats.frequencies[intv] += 1;
}
else {
stats.frequencies[intv] = 1;
}
}
sum += intv;
}
else {
d[j][1] = new $.jsDate(d[j][1]).getTime();
pd[j][1] = new $.jsDate(d[j][1]).getTime();
sd[j][1] = new $.jsDate(d[j][1]).getTime();
if ((d[j][1] != null && d[j][1] < db.min) || db.min == null) {
db.min = d[j][1];
}
if ((d[j][1] != null && d[j][1] > db.max) || db.max == null) {
db.max = d[j][1];
}
if (j>0) {
intv = Math.abs(d[j][1] - d[j-1][1]);
stats.intervals.push(intv);
if (stats.frequencies.hasOwnProperty(intv)) {
stats.frequencies[intv] += 1;
}
else {
stats.frequencies[intv] = 1;
}
}
}
sum += intv;
}
if (s.renderer.bands) {
if (s.renderer.bands.hiData.length) {
var bd = s.renderer.bands.hiData;
for (var j=0, l=bd.length; j < l; j++) {
if (this.name === 'xaxis' || this.name === 'x2axis') {
bd[j][0] = new $.jsDate(bd[j][0]).getTime();
if ((bd[j][0] != null && bd[j][0] > db.max) || db.max == null) {
db.max = bd[j][0];
}
}
else {
bd[j][1] = new $.jsDate(bd[j][1]).getTime();
if ((bd[j][1] != null && bd[j][1] > db.max) || db.max == null) {
db.max = bd[j][1];
}
}
}
}
if (s.renderer.bands.lowData.length) {
var bd = s.renderer.bands.lowData;
for (var j=0, l=bd.length; j < l; j++) {
if (this.name === 'xaxis' || this.name === 'x2axis') {
bd[j][0] = new $.jsDate(bd[j][0]).getTime();
if ((bd[j][0] != null && bd[j][0] < db.min) || db.min == null) {
db.min = bd[j][0];
}
}
else {
bd[j][1] = new $.jsDate(bd[j][1]).getTime();
if ((bd[j][1] != null && bd[j][1] < db.min) || db.min == null) {
db.min = bd[j][1];
}
}
}
}
}
var tempf = 0,
tempn=0;
for (var n in stats.frequencies) {
stats.sortedIntervals.push({interval:n, frequency:stats.frequencies[n]});
}
stats.sortedIntervals.sort(function(a, b){
return b.frequency - a.frequency;
});
stats.min = $.jqplot.arrayMin(stats.intervals);
stats.max = $.jqplot.arrayMax(stats.intervals);
stats.mean = sum/d.length;
this._intervalStats.push(stats);
stats = sum = s = d = pd = sd = null;
}
db = null;
};
// called with scope of an axis
$.jqplot.DateAxisRenderer.prototype.reset = function() {
this.min = this._options.min;
this.max = this._options.max;
this.tickInterval = this._options.tickInterval;
this.numberTicks = this._options.numberTicks;
this._autoFormatString = '';
if (this._overrideFormatString && this.tickOptions && this.tickOptions.formatString) {
this.tickOptions.formatString = '';
}
this.daTickInterval = this._daTickInterval;
// this._ticks = this.__ticks;
};
$.jqplot.DateAxisRenderer.prototype.createTicks = function(plot) {
// we're are operating on an axis here
var ticks = this._ticks;
var userTicks = this.ticks;
var name = this.name;
// databounds were set on axis initialization.
var db = this._dataBounds;
var iv = this._intervalStats;
var dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
var interval;
var min, max;
var pos1, pos2;
var tt, i;
var threshold = 30;
var insetMult = 1;
var daTickInterval = null;
// if user specified a tick interval, convert to usable.
if (this.tickInterval != null)
{
// if interval is a number or can be converted to one, use it.
// Assume it is in SECONDS!!!
if (Number(this.tickInterval)) {
daTickInterval = [Number(this.tickInterval), 'seconds'];
}
// else, parse out something we can build from.
else if (typeof this.tickInterval == "string") {
var parts = this.tickInterval.split(' ');
if (parts.length == 1) {
daTickInterval = [1, parts[0]];
}
else if (parts.length == 2) {
daTickInterval = [parts[0], parts[1]];
}
}
}
var tickInterval = this.tickInterval;
// if we already have ticks, use them.
// ticks must be in order of increasing value.
min = new $.jsDate((this.min != null) ? this.min : db.min).getTime();
max = new $.jsDate((this.max != null) ? this.max : db.max).getTime();
// see if we're zooming. if we are, don't use the min and max we're given,
// but compute some nice ones. They will be reset later.
var cursor = plot.plugins.cursor;
if (cursor && cursor._zoom && cursor._zoom.zooming) {
this.min = null;
this.max = null;
}
var range = max - min;
if (this.tickOptions == null || !this.tickOptions.formatString) {
this._overrideFormatString = true;
}
if (userTicks.length) {
// ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
for (i=0; i<userTicks.length; i++){
var ut = userTicks[i];
var t = new this.tickRenderer(this.tickOptions);
if (ut.constructor == Array) {
t.value = new $.jsDate(ut[0]).getTime();
t.label = ut[1];
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(t.value, this.name);
this._ticks.push(t);
}
else {
t.value = new $.jsDate(ut).getTime();
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(t.value, this.name);
this._ticks.push(t);
}
}
this.numberTicks = userTicks.length;
this.min = this._ticks[0].value;
this.max = this._ticks[this.numberTicks-1].value;
this.daTickInterval = [(this.max - this.min) / (this.numberTicks - 1)/1000, 'seconds'];
}
////////
// We don't have any ticks yet, let's make some!
////////
// special case when there is only one point, make three tick marks to center the point
else if (this.min == null && this.max == null && db.min == db.max)
{
var onePointOpts = $.extend(true, {}, this.tickOptions, {name: this.name, value: null});
var delta = 300000;
this.min = db.min - delta;
this.max = db.max + delta;
this.numberTicks = 3;
for(var i=this.min;i<=this.max;i+= delta)
{
onePointOpts.value = i;
var t = new this.tickRenderer(onePointOpts);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
t.showLabel = false;
t.showMark = false;
this._ticks.push(t);
}
if(this.showTicks) {
this._ticks[1].showLabel = true;
}
if(this.showTickMarks) {
this._ticks[1].showTickMarks = true;
}
}
// if user specified min and max are null, we set those to make best ticks.
else if (this.min == null && this.max == null) {
var opts = $.extend(true, {}, this.tickOptions, {name: this.name, value: null});
// want to find a nice interval
var nttarget,
titarget;
// if no tickInterval or numberTicks options specified, make a good guess.
if (!this.tickInterval && !this.numberTicks) {
var tdim = Math.max(dim, threshold+1);
// how many ticks to put on the axis?
// date labels tend to be long. If ticks not rotated,
// don't use too many and have a high spacing factor.
// If we are rotating ticks, use a lower factor.
var spacingFactor = 115;
if (this.tickRenderer === $.jqplot.CanvasAxisTickRenderer && this.tickOptions.angle) {
spacingFactor = 115 - 40 * Math.abs(Math.sin(this.tickOptions.angle/180*Math.PI));
}
nttarget = Math.ceil((tdim-threshold)/spacingFactor + 1);
titarget = (max - min) / (nttarget - 1);
}
// If tickInterval is specified, we'll try to honor it.
// Not guaranteed to get this interval, but we'll get as close as
// we can.
// tickInterval will be used before numberTicks, that is if
// both are specified, numberTicks will be ignored.
else if (this.tickInterval) {
titarget = new $.jsDate(0).add(daTickInterval[0], daTickInterval[1]).getTime();
}
// if numberTicks specified, try to honor it.
// Not guaranteed, but will try to get close.
else if (this.numberTicks) {
nttarget = this.numberTicks;
titarget = (max - min) / (nttarget - 1);
}
// If we can use an interval of 2 weeks or less, pick best one
if (titarget <= 19*day) {
var ret = bestDateInterval(min, max, titarget);
var tempti = ret[0];
this._autoFormatString = ret[1];
min = new $.jsDate(min);
min = Math.floor((min.getTime() - min.getUtcOffset())/tempti) * tempti + min.getUtcOffset();
nttarget = Math.ceil((max - min) / tempti) + 1;
this.min = min;
this.max = min + (nttarget - 1) * tempti;
// if max is less than max, add an interval
if (this.max < max) {
this.max += tempti;
nttarget += 1;
}
this.tickInterval = tempti;
this.numberTicks = nttarget;
for (var i=0; i<nttarget; i++) {
opts.value = this.min + i * tempti;
t = new this.tickRenderer(opts);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
this._ticks.push(t);
}
insetMult = this.tickInterval;
}
// should we use a monthly interval?
else if (titarget <= 9 * month) {
this._autoFormatString = '%v';
// how many months in an interval?
var intv = Math.round(titarget/month);
if (intv < 1) {
intv = 1;
}
else if (intv > 6) {
intv = 6;
}
// figure out the starting month and ending month.
var mstart = new $.jsDate(min).setDate(1).setHours(0,0,0,0);
// See if max ends exactly on a month
var tempmend = new $.jsDate(max);
var mend = new $.jsDate(max).setDate(1).setHours(0,0,0,0);
if (tempmend.getTime() !== mend.getTime()) {
mend = mend.add(1, 'month');
}
var nmonths = mend.diff(mstart, 'month');
nttarget = Math.ceil(nmonths/intv) + 1;
this.min = mstart.getTime();
this.max = mstart.clone().add((nttarget - 1) * intv, 'month').getTime();
this.numberTicks = nttarget;
for (var i=0; i<nttarget; i++) {
if (i === 0) {
opts.value = mstart.getTime();
}
else {
opts.value = mstart.add(intv, 'month').getTime();
}
t = new this.tickRenderer(opts);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
this._ticks.push(t);
}
insetMult = intv * month;
}
// use yearly intervals
else {
this._autoFormatString = '%v';
// how many years in an interval?
var intv = Math.round(titarget/year);
if (intv < 1) {
intv = 1;
}
// figure out the starting and ending years.
var mstart = new $.jsDate(min).setMonth(0, 1).setHours(0,0,0,0);
var mend = new $.jsDate(max).add(1, 'year').setMonth(0, 1).setHours(0,0,0,0);
var nyears = mend.diff(mstart, 'year');
nttarget = Math.ceil(nyears/intv) + 1;
this.min = mstart.getTime();
this.max = mstart.clone().add((nttarget - 1) * intv, 'year').getTime();
this.numberTicks = nttarget;
for (var i=0; i<nttarget; i++) {
if (i === 0) {
opts.value = mstart.getTime();
}
else {
opts.value = mstart.add(intv, 'year').getTime();
}
t = new this.tickRenderer(opts);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
this._ticks.push(t);
}
insetMult = intv * year;
}
}
////////
// Some option(s) specified, work around that.
////////
else {
if (name == 'xaxis' || name == 'x2axis') {
dim = this._plotDimensions.width;
}
else {
dim = this._plotDimensions.height;
}
// if min, max and number of ticks specified, user can't specify interval.
if (this.min != null && this.max != null && this.numberTicks != null) {
this.tickInterval = null;
}
if (this.tickInterval != null && daTickInterval != null) {
this.daTickInterval = daTickInterval;
}
// if min and max are same, space them out a bit
if (min == max) {
var adj = 24*60*60*500; // 1/2 day
min -= adj;
max += adj;
}
range = max - min;
var optNumTicks = 2 + parseInt(Math.max(0, dim-100)/100, 10);
var rmin, rmax;
rmin = (this.min != null) ? new $.jsDate(this.min).getTime() : min - range/2*(this.padMin - 1);
rmax = (this.max != null) ? new $.jsDate(this.max).getTime() : max + range/2*(this.padMax - 1);
this.min = rmin;
this.max = rmax;
range = this.max - this.min;
if (this.numberTicks == null){
// if tickInterval is specified by user, we will ignore computed maximum.
// max will be equal or greater to fit even # of ticks.
if (this.daTickInterval != null) {
var nc = new $.jsDate(this.max).diff(this.min, this.daTickInterval[1], true);
this.numberTicks = Math.ceil(nc/this.daTickInterval[0]) +1;
// this.max = new $.jsDate(this.min).add(this.numberTicks-1, this.daTickInterval[1]).getTime();
this.max = new $.jsDate(this.min).add((this.numberTicks-1) * this.daTickInterval[0], this.daTickInterval[1]).getTime();
}
else if (dim > 200) {
this.numberTicks = parseInt(3+(dim-200)/100, 10);
}
else {
this.numberTicks = 2;
}
}
insetMult = range / (this.numberTicks-1)/1000;
if (this.daTickInterval == null) {
this.daTickInterval = [insetMult, 'seconds'];
}
for (var i=0; i<this.numberTicks; i++){
var min = new $.jsDate(this.min);
tt = min.add(i*this.daTickInterval[0], this.daTickInterval[1]).getTime();
var t = new this.tickRenderer(this.tickOptions);
// var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(tt, this.name);
this._ticks.push(t);
}
}
if (this.tickInset) {
this.min = this.min - this.tickInset * insetMult;
this.max = this.max + this.tickInset * insetMult;
}
if (this._daTickInterval == null) {
this._daTickInterval = this.daTickInterval;
}
ticks = null;
};
})(jQuery);

View File

@@ -0,0 +1,816 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.DonutRenderer
* Plugin renderer to draw a donut chart.
* x values, if present, will be used as slice labels.
* y values give slice size.
*
* To use this renderer, you need to include the
* donut renderer plugin, for example:
*
* > <script type="text/javascript" src="plugins/jqplot.donutRenderer.js"></script>
*
* Properties described here are passed into the $.jqplot function
* as options on the series renderer. For example:
*
* > plot2 = $.jqplot('chart2', [s1, s2], {
* > seriesDefaults: {
* > renderer:$.jqplot.DonutRenderer,
* > rendererOptions:{
* > sliceMargin: 2,
* > innerDiameter: 110,
* > startAngle: -90
* > }
* > }
* > });
*
* A donut plot will trigger events on the plot target
* according to user interaction. All events return the event object,
* the series index, the point (slice) index, and the point data for
* the appropriate slice.
*
* 'jqplotDataMouseOver' - triggered when user mouseing over a slice.
* 'jqplotDataHighlight' - triggered the first time user mouses over a slice,
* if highlighting is enabled.
* 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
* a highlighted slice.
* 'jqplotDataClick' - triggered when the user clicks on a slice.
* 'jqplotDataRightClick' - tiggered when the user right clicks on a slice if
* the "captureRightClick" option is set to true on the plot.
*/
$.jqplot.DonutRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.DonutRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.DonutRenderer.prototype.constructor = $.jqplot.DonutRenderer;
// called with scope of a series
$.jqplot.DonutRenderer.prototype.init = function(options, plot) {
// Group: Properties
//
// prop: diameter
// Outer diameter of the donut, auto computed by default
this.diameter = null;
// prop: innerDiameter
// Inner diameter of the donut, auto calculated by default.
// If specified will override thickness value.
this.innerDiameter = null;
// prop: thickness
// thickness of the donut, auto computed by default
// Overridden by if innerDiameter is specified.
this.thickness = null;
// prop: padding
// padding between the donut and plot edges, legend, etc.
this.padding = 20;
// prop: sliceMargin
// angular spacing between donut slices in degrees.
this.sliceMargin = 0;
// prop: ringMargin
// pixel distance between rings, or multiple series in a donut plot.
// null will compute ringMargin based on sliceMargin.
this.ringMargin = null;
// prop: fill
// true or false, whether to fil the slices.
this.fill = true;
// prop: shadowOffset
// offset of the shadow from the slice and offset of
// each succesive stroke of the shadow from the last.
this.shadowOffset = 2;
// prop: shadowAlpha
// transparency of the shadow (0 = transparent, 1 = opaque)
this.shadowAlpha = 0.07;
// prop: shadowDepth
// number of strokes to apply to the shadow,
// each stroke offset shadowOffset from the last.
this.shadowDepth = 5;
// prop: highlightMouseOver
// True to highlight slice when moused over.
// This must be false to enable highlightMouseDown to highlight when clicking on a slice.
this.highlightMouseOver = true;
// prop: highlightMouseDown
// True to highlight when a mouse button is pressed over a slice.
// This will be disabled if highlightMouseOver is true.
this.highlightMouseDown = false;
// prop: highlightColors
// an array of colors to use when highlighting a slice.
this.highlightColors = [];
// prop: dataLabels
// Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
// Defaults to percentage of each pie slice.
this.dataLabels = 'percent';
// prop: showDataLabels
// true to show data labels on slices.
this.showDataLabels = false;
// prop: totalLabel
// true to show total label in the centre
this.totalLabel = false;
// prop: dataLabelFormatString
// Format string for data labels. If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
this.dataLabelFormatString = null;
// prop: dataLabelThreshold
// Threshhold in percentage (0 - 100) of pie area, below which no label will be displayed.
// This applies to all label types, not just to percentage labels.
this.dataLabelThreshold = 3;
// prop: dataLabelPositionFactor
// A Multiplier (0-1) of the pie radius which controls position of label on slice.
// Increasing will slide label toward edge of pie, decreasing will slide label toward center of pie.
this.dataLabelPositionFactor = 0.4;
// prop: dataLabelNudge
// Number of pixels to slide the label away from (+) or toward (-) the center of the pie.
this.dataLabelNudge = 0;
// prop: startAngle
// Angle to start drawing donut in degrees.
// According to orientation of canvas coordinate system:
// 0 = on the positive x axis
// -90 = on the positive y axis.
// 90 = on the negaive y axis.
// 180 or - 180 = on the negative x axis.
this.startAngle = 0;
this.tickRenderer = $.jqplot.DonutTickRenderer;
// Used as check for conditions where donut shouldn't be drawn.
this._drawData = true;
this._type = 'donut';
// if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
if (options.highlightMouseDown && options.highlightMouseOver == null) {
options.highlightMouseOver = false;
}
$.extend(true, this, options);
if (this.diameter != null) {
this.diameter = this.diameter - this.sliceMargin;
}
this._diameter = null;
this._innerDiameter = null;
this._radius = null;
this._innerRadius = null;
this._thickness = null;
// references to the previous series in the plot to properly calculate diameters
// and thicknesses of nested rings.
this._previousSeries = [];
this._numberSeries = 1;
// array of [start,end] angles arrays, one for each slice. In radians.
this._sliceAngles = [];
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// set highlight colors if none provided
if (this.highlightColors.length == 0) {
for (var i=0; i<this.seriesColors.length; i++){
var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var sum = newrgb[0] + newrgb[1] + newrgb[2];
for (var j=0; j<3; j++) {
// when darkening, lowest color component can be is 60.
newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
newrgb[j] = parseInt(newrgb[j], 10);
}
this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
}
}
plot.postParseOptionsHooks.addOnce(postParseOptions);
plot.postInitHooks.addOnce(postInit);
plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
plot.postDrawHooks.addOnce(postPlotDraw);
};
$.jqplot.DonutRenderer.prototype.setGridData = function(plot) {
// set gridData property. This will hold angle in radians of each data point.
var stack = [];
var td = [];
var sa = this.startAngle/180*Math.PI;
var tot = 0;
// don't know if we have any valid data yet, so set plot to not draw.
this._drawData = false;
for (var i=0; i<this.data.length; i++){
if (this.data[i][1] != 0) {
// we have data, O.K. to draw.
this._drawData = true;
}
stack.push(this.data[i][1]);
td.push([this.data[i][0]]);
if (i>0) {
stack[i] += stack[i-1];
}
tot += this.data[i][1];
}
var fact = Math.PI*2/stack[stack.length - 1];
for (var i=0; i<stack.length; i++) {
td[i][1] = stack[i] * fact;
td[i][2] = this.data[i][1]/tot;
}
this.gridData = td;
};
$.jqplot.DonutRenderer.prototype.makeGridData = function(data, plot) {
var stack = [];
var td = [];
var tot = 0;
var sa = this.startAngle/180*Math.PI;
// don't know if we have any valid data yet, so set plot to not draw.
this._drawData = false;
for (var i=0; i<data.length; i++){
if (this.data[i][1] != 0) {
// we have data, O.K. to draw.
this._drawData = true;
}
stack.push(data[i][1]);
td.push([data[i][0]]);
if (i>0) {
stack[i] += stack[i-1];
}
tot += data[i][1];
}
var fact = Math.PI*2/stack[stack.length - 1];
for (var i=0; i<stack.length; i++) {
td[i][1] = stack[i] * fact;
td[i][2] = data[i][1]/tot;
}
this._totalAmount = tot;
return td;
};
$.jqplot.DonutRenderer.prototype.drawSlice = function (ctx, ang1, ang2, color, isShadow) {
var r = this._diameter / 2;
var ri = r - this._thickness;
var fill = this.fill;
// var lineWidth = this.lineWidth;
ctx.save();
ctx.translate(this._center[0], this._center[1]);
// ctx.translate(this.sliceMargin*Math.cos((ang1+ang2)/2), this.sliceMargin*Math.sin((ang1+ang2)/2));
if (isShadow) {
for (var i=0; i<this.shadowDepth; i++) {
ctx.save();
ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
doDraw();
}
}
else {
doDraw();
}
function doDraw () {
// Fix for IE and Chrome that can't seem to draw circles correctly.
// ang2 should always be <= 2 pi since that is the way the data is converted.
if (ang2 > 6.282 + this.startAngle) {
ang2 = 6.282 + this.startAngle;
if (ang1 > ang2) {
ang1 = 6.281 + this.startAngle;
}
}
// Fix for IE, where it can't seem to handle 0 degree angles. Also avoids
// ugly line on unfilled donuts.
if (ang1 >= ang2) {
return;
}
ctx.beginPath();
ctx.fillStyle = color;
ctx.strokeStyle = color;
// ctx.lineWidth = lineWidth;
ctx.arc(0, 0, r, ang1, ang2, false);
ctx.lineTo(ri*Math.cos(ang2), ri*Math.sin(ang2));
ctx.arc(0,0, ri, ang2, ang1, true);
ctx.closePath();
if (fill) {
ctx.fill();
}
else {
ctx.stroke();
}
}
if (isShadow) {
for (var i=0; i<this.shadowDepth; i++) {
ctx.restore();
}
}
ctx.restore();
};
// called with scope of series
$.jqplot.DonutRenderer.prototype.draw = function (ctx, gd, options, plot) {
var i;
var opts = (options != undefined) ? options : {};
// offset and direction of offset due to legend placement
var offx = 0;
var offy = 0;
var trans = 1;
// var colorGenerator = new this.colorGenerator(this.seriesColors);
if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
var li = options.legendInfo;
switch (li.location) {
case 'nw':
offx = li.width + li.xoffset;
break;
case 'w':
offx = li.width + li.xoffset;
break;
case 'sw':
offx = li.width + li.xoffset;
break;
case 'ne':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'e':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'se':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'n':
offy = li.height + li.yoffset;
break;
case 's':
offy = li.height + li.yoffset;
trans = -1;
break;
default:
break;
}
}
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
//see http://stackoverflow.com/questions/20221461/hidpi-retina-plot-drawing
var cw = parseInt(ctx.canvas.style.width);
var ch = parseInt(ctx.canvas.style.height);
var w = cw - offx - 2 * this.padding;
var h = ch - offy - 2 * this.padding;
var mindim = Math.min(w,h);
var d = mindim;
var ringmargin = (this.ringMargin == null) ? this.sliceMargin * 2.0 : this.ringMargin;
for (var i=0; i<this._previousSeries.length; i++) {
d -= 2.0 * this._previousSeries[i]._thickness + 2.0 * ringmargin;
}
this._diameter = this.diameter || d;
if (this.innerDiameter != null) {
var od = (this._numberSeries > 1 && this.index > 0) ? this._previousSeries[0]._diameter : this._diameter;
this._thickness = this.thickness || (od - this.innerDiameter - 2.0*ringmargin*this._numberSeries) / this._numberSeries/2.0;
}
else {
this._thickness = this.thickness || mindim / 2 / (this._numberSeries + 1) * 0.85;
}
if (this._diameter < 6) {
$.jqplot.log("Diameter of donut too small, not rendering.");
return;
}
var r = this._radius = this._diameter/2;
this._innerRadius = this._radius - this._thickness;
var sa = this.startAngle / 180 * Math.PI;
this._center = [(cw - trans * offx)/2 + trans * offx, (ch - trans*offy)/2 + trans * offy];
if (this.shadow) {
var shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
for (var i=0; i<gd.length; i++) {
var ang1 = (i == 0) ? sa : gd[i-1][1] + sa;
// Adjust ang1 and ang2 for sliceMargin
ang1 += this.sliceMargin/180*Math.PI;
this.renderer.drawSlice.call (this, ctx, ang1, gd[i][1]+sa, shadowColor, true);
}
}
for (var i=0; i<gd.length; i++) {
var ang1 = (i == 0) ? sa : gd[i-1][1] + sa;
// Adjust ang1 and ang2 for sliceMargin
ang1 += this.sliceMargin/180*Math.PI;
var ang2 = gd[i][1] + sa;
this._sliceAngles.push([ang1, ang2]);
this.renderer.drawSlice.call (this, ctx, ang1, ang2, this.seriesColors[i], false);
if (this.showDataLabels && gd[i][2]*100 >= this.dataLabelThreshold) {
var fstr, avgang = (ang1+ang2)/2, label;
if (this.dataLabels == 'label') {
fstr = this.dataLabelFormatString || '%s';
label = $.jqplot.sprintf(fstr, gd[i][0]);
}
else if (this.dataLabels == 'value') {
fstr = this.dataLabelFormatString || '%d';
label = $.jqplot.sprintf(fstr, this.data[i][1]);
}
else if (this.dataLabels == 'percent') {
fstr = this.dataLabelFormatString || '%d%%';
label = $.jqplot.sprintf(fstr, gd[i][2]*100);
}
else if (this.dataLabels.constructor == Array) {
fstr = this.dataLabelFormatString || '%s';
label = $.jqplot.sprintf(fstr, this.dataLabels[i]);
}
var fact = this._innerRadius + this._thickness * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
var x = this._center[0] + Math.cos(avgang) * fact + this.canvas._offsets.left;
var y = this._center[1] + Math.sin(avgang) * fact + this.canvas._offsets.top;
var labelelem = $('<span class="jqplot-donut-series jqplot-data-label" style="position:absolute;">' + label + '</span>').insertBefore(plot.eventCanvas._elem);
x -= labelelem.width()/2;
y -= labelelem.height()/2;
x = Math.round(x);
y = Math.round(y);
labelelem.css({left: x, top: y});
}
}
if (this.totalLabel) {
var totalLabel = $('<div class="jqplot-data-label" style="position:absolute">' + this._totalAmount + '</div>').insertAfter(plot.eventCanvas._elem);
totalLabel.css({left: this._center[0], top: this._center[1]});
}
};
$.jqplot.DonutAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
};
$.jqplot.DonutAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.DonutAxisRenderer.prototype.constructor = $.jqplot.DonutAxisRenderer;
// There are no traditional axes on a donut chart. We just need to provide
// dummy objects with properties so the plot will render.
// called with scope of axis object.
$.jqplot.DonutAxisRenderer.prototype.init = function(options){
//
this.tickRenderer = $.jqplot.DonutTickRenderer;
$.extend(true, this, options);
// I don't think I'm going to need _dataBounds here.
// have to go Axis scaling in a way to fit chart onto plot area
// and provide u2p and p2u functionality for mouse cursor, etc.
// for convienence set _dataBounds to 0 and 100 and
// set min/max to 0 and 100.
this._dataBounds = {min:0, max:100};
this.min = 0;
this.max = 100;
this.showTicks = false;
this.ticks = [];
this.showMark = false;
this.show = false;
};
$.jqplot.DonutLegendRenderer = function(){
$.jqplot.TableLegendRenderer.call(this);
};
$.jqplot.DonutLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
$.jqplot.DonutLegendRenderer.prototype.constructor = $.jqplot.DonutLegendRenderer;
/**
* Class: $.jqplot.DonutLegendRenderer
* Legend Renderer specific to donut plots. Set by default
* when user creates a donut plot.
*/
$.jqplot.DonutLegendRenderer.prototype.init = function(options) {
// Group: Properties
//
// prop: numberRows
// Maximum number of rows in the legend. 0 or null for unlimited.
this.numberRows = null;
// prop: numberColumns
// Maximum number of columns in the legend. 0 or null for unlimited.
this.numberColumns = null;
$.extend(true, this, options);
};
// called with context of legend
$.jqplot.DonutLegendRenderer.prototype.draw = function() {
var legend = this;
if (this.show) {
var series = this._series;
var ss = 'position:absolute;';
ss += (this.background) ? 'background:'+this.background+';' : '';
ss += (this.border) ? 'border:'+this.border+';' : '';
ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
// Donut charts legends don't go by number of series, but by number of data points
// in the series. Refactor things here for that.
var pad = false,
reverse = false,
nr, nc;
var s = series[0];
var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
if (s.show) {
var pd = s.data;
if (this.numberRows) {
nr = this.numberRows;
if (!this.numberColumns){
nc = Math.ceil(pd.length/nr);
}
else{
nc = this.numberColumns;
}
}
else if (this.numberColumns) {
nc = this.numberColumns;
nr = Math.ceil(pd.length/this.numberColumns);
}
else {
nr = pd.length;
nc = 1;
}
var i, j, tr, td1, td2, lt, rs, color;
var idx = 0;
for (i=0; i<nr; i++) {
if (reverse){
tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
}
else{
tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
}
for (j=0; j<nc; j++) {
if (idx < pd.length){
lt = this.labels[idx] || pd[idx][0].toString();
color = colorGenerator.next();
if (!reverse){
if (i>0){
pad = true;
}
else{
pad = false;
}
}
else{
if (i == nr -1){
pad = false;
}
else{
pad = true;
}
}
rs = (pad) ? this.rowSpacing : '0';
td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
'<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
'</div></td>');
td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
if (this.escapeHtml){
td2.text(lt);
}
else {
td2.html(lt);
}
if (reverse) {
td2.prependTo(tr);
td1.prependTo(tr);
}
else {
td1.appendTo(tr);
td2.appendTo(tr);
}
pad = true;
}
idx++;
}
}
}
}
return this._elem;
};
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = options.axesDefaults || {};
options.legend = options.legend || {};
options.seriesDefaults = options.seriesDefaults || {};
// only set these if there is a donut series
var setopts = false;
if (options.seriesDefaults.renderer == $.jqplot.DonutRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer == $.jqplot.DonutRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.DonutAxisRenderer;
options.legend.renderer = $.jqplot.DonutLegendRenderer;
options.legend.preDraw = true;
options.seriesDefaults.pointLabels = {show: false};
}
}
// called with scope of plot.
function postInit(target, data, options) {
// if multiple series, add a reference to the previous one so that
// donut rings can nest.
for (var i=1; i<this.series.length; i++) {
if (!this.series[i]._previousSeries.length){
for (var j=0; j<i; j++) {
if (this.series[i].renderer.constructor == $.jqplot.DonutRenderer && this.series[j].renderer.constructor == $.jqplot.DonutRenderer) {
this.series[i]._previousSeries.push(this.series[j]);
}
}
}
}
for (i=0; i<this.series.length; i++) {
if (this.series[i].renderer.constructor == $.jqplot.DonutRenderer) {
this.series[i]._numberSeries = this.series.length;
// don't allow mouseover and mousedown at same time.
if (this.series[i].highlightMouseOver) {
this.series[i].highlightMouseDown = false;
}
}
}
}
var postParseOptionsRun = false;
// called with scope of plot
function postParseOptions(options) {
for (var i=0; i<this.series.length; i++) {
this.series[i].seriesColors = this.seriesColors;
this.series[i].colorGenerator = $.jqplot.colorGenerator;
}
}
function highlight (plot, sidx, pidx) {
var s = plot.series[sidx];
var canvas = plot.plugins.donutRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
s._highlightedPoint = pidx;
plot.plugins.donutRenderer.highlightedSeriesIndex = sidx;
s.renderer.drawSlice.call(s, canvas._ctx, s._sliceAngles[pidx][0], s._sliceAngles[pidx][1], s.highlightColors[pidx], false);
}
function unhighlight (plot) {
var canvas = plot.plugins.donutRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
for (var i=0; i<plot.series.length; i++) {
plot.series[i]._highlightedPoint = null;
}
plot.plugins.donutRenderer.highlightedSeriesIndex = null;
plot.target.trigger('jqplotDataUnhighlight');
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt1 = jQuery.Event('jqplotDataMouseOver');
evt1.pageX = ev.pageX;
evt1.pageY = ev.pageY;
plot.target.trigger(evt1, ins);
if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.donutRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.donutRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
var idx = plot.plugins.donutRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
}
function handleClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt = jQuery.Event('jqplotDataClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var idx = plot.plugins.donutRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
var evt = jQuery.Event('jqplotDataRightClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
function postPlotDraw() {
// Memory Leaks patch
if (this.plugins.donutRenderer && this.plugins.donutRenderer.highlightCanvas) {
this.plugins.donutRenderer.highlightCanvas.resetCanvas();
this.plugins.donutRenderer.highlightCanvas = null;
}
this.plugins.donutRenderer = {highlightedSeriesIndex:null};
this.plugins.donutRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
// do we have any data labels? if so, put highlight canvas before those
// Fix for broken jquery :first selector with canvas (VML) elements.
var labels = $(this.targetId+' .jqplot-data-label');
if (labels.length) {
$(labels[0]).before(this.plugins.donutRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-donutRenderer-highlight-canvas', this._plotDimensions, this));
}
// else put highlight canvas before event canvas.
else {
this.eventCanvas._elem.before(this.plugins.donutRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-donutRenderer-highlight-canvas', this._plotDimensions, this));
}
var hctx = this.plugins.donutRenderer.highlightCanvas.setContext();
this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
}
$.jqplot.preInitHooks.push(preInit);
$.jqplot.DonutTickRenderer = function() {
$.jqplot.AxisTickRenderer.call(this);
};
$.jqplot.DonutTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
$.jqplot.DonutTickRenderer.prototype.constructor = $.jqplot.DonutTickRenderer;
})(jQuery);

View File

@@ -0,0 +1,225 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.Dragable
* Plugin to make plotted points dragable by the user.
*/
$.jqplot.Dragable = function(options) {
// Group: Properties
this.markerRenderer = new $.jqplot.MarkerRenderer({shadow:false});
this.shapeRenderer = new $.jqplot.ShapeRenderer();
this.isDragging = false;
this.isOver = false;
this._ctx;
this._elem;
this._point;
this._gridData;
// prop: color
// CSS color spec for the dragged point (and adjacent line segment or bar).
this.color;
// prop: constrainTo
// Constrain dragging motion to an axis or to none.
// Allowable values are 'none', 'x', 'y'
this.constrainTo = 'none'; // 'x', 'y', or 'none';
$.extend(true, this, options);
};
function DragCanvas() {
$.jqplot.GenericCanvas.call(this);
this.isDragging = false;
this.isOver = false;
this._neighbor;
this._cursors = [];
}
DragCanvas.prototype = new $.jqplot.GenericCanvas();
DragCanvas.prototype.constructor = DragCanvas;
// called within scope of series
$.jqplot.Dragable.parseOptions = function (defaults, opts) {
var options = opts || {};
this.plugins.dragable = new $.jqplot.Dragable(options.dragable);
// since this function is called before series options are parsed,
// we can set this here and it will be overridden if needed.
this.isDragable = $.jqplot.config.enablePlugins;
};
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
// add a new DragCanvas object to the plot plugins to handle drawing on this new canvas.
$.jqplot.Dragable.postPlotDraw = function() {
// Memory Leaks patch
if (this.plugins.dragable && this.plugins.dragable.highlightCanvas) {
this.plugins.dragable.highlightCanvas.resetCanvas();
this.plugins.dragable.highlightCanvas = null;
}
this.plugins.dragable = {previousCursor:'auto', isOver:false};
this.plugins.dragable.dragCanvas = new DragCanvas();
this.eventCanvas._elem.before(this.plugins.dragable.dragCanvas.createElement(this._gridPadding, 'jqplot-dragable-canvas', this._plotDimensions, this));
var dctx = this.plugins.dragable.dragCanvas.setContext();
};
//$.jqplot.preInitHooks.push($.jqplot.Dragable.init);
$.jqplot.preParseSeriesOptionsHooks.push($.jqplot.Dragable.parseOptions);
$.jqplot.postDrawHooks.push($.jqplot.Dragable.postPlotDraw);
$.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);
$.jqplot.eventListenerHooks.push(['jqplotMouseDown', handleDown]);
$.jqplot.eventListenerHooks.push(['jqplotMouseUp', handleUp]);
function initDragPoint(plot, neighbor) {
var s = plot.series[neighbor.seriesIndex];
var drag = s.plugins.dragable;
// first, init the mark renderer for the dragged point
var smr = s.markerRenderer;
var mr = drag.markerRenderer;
mr.style = smr.style;
mr.lineWidth = smr.lineWidth + 2.5;
mr.size = smr.size + 5;
if (!drag.color) {
var rgba = $.jqplot.getColorComponents(smr.color);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var alpha = (rgba[3] >= 0.6) ? rgba[3]*0.6 : rgba[3]*(2-rgba[3]);
drag.color = 'rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+','+alpha+')';
}
mr.color = drag.color;
mr.init();
var start = (neighbor.pointIndex > 0) ? neighbor.pointIndex - 1 : 0;
var end = neighbor.pointIndex+2;
drag._gridData = s.gridData.slice(start, end);
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (plot.plugins.dragable.dragCanvas.isDragging) {
var dc = plot.plugins.dragable.dragCanvas;
var dp = dc._neighbor;
var s = plot.series[dp.seriesIndex];
var drag = s.plugins.dragable;
var gd = s.gridData;
// compute the new grid position with any constraints.
var x = (drag.constrainTo == 'y') ? dp.gridData[0] : gridpos.x;
var y = (drag.constrainTo == 'x') ? dp.gridData[1] : gridpos.y;
// compute data values for any listeners.
var xu = s._xaxis.series_p2u(x);
var yu = s._yaxis.series_p2u(y);
// clear the canvas then redraw effect at new position.
var ctx = dc._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
// adjust our gridData for the new mouse position
if (dp.pointIndex > 0) {
drag._gridData[1] = [x, y];
}
else {
drag._gridData[0] = [x, y];
}
plot.series[dp.seriesIndex].draw(dc._ctx, {gridData:drag._gridData, shadow:false, preventJqPlotSeriesDrawTrigger:true, color:drag.color, markerOptions:{color:drag.color, shadow:false}, trendline:{show:false}});
plot.target.trigger('jqplotSeriesPointChange', [dp.seriesIndex, dp.pointIndex, [xu,yu], [x,y]]);
}
else if (neighbor != null) {
var series = plot.series[neighbor.seriesIndex];
if (series.isDragable) {
var dc = plot.plugins.dragable.dragCanvas;
if (!dc.isOver) {
dc._cursors.push(ev.target.style.cursor);
ev.target.style.cursor = "pointer";
}
dc.isOver = true;
}
}
else if (neighbor == null) {
var dc = plot.plugins.dragable.dragCanvas;
if (dc.isOver) {
ev.target.style.cursor = dc._cursors.pop();
dc.isOver = false;
}
}
}
function handleDown(ev, gridpos, datapos, neighbor, plot) {
var dc = plot.plugins.dragable.dragCanvas;
dc._cursors.push(ev.target.style.cursor);
if (neighbor != null) {
var s = plot.series[neighbor.seriesIndex];
var drag = s.plugins.dragable;
if (s.isDragable && !dc.isDragging) {
dc._neighbor = neighbor;
dc.isDragging = true;
initDragPoint(plot, neighbor);
drag.markerRenderer.draw(s.gridData[neighbor.pointIndex][0], s.gridData[neighbor.pointIndex][1], dc._ctx);
ev.target.style.cursor = "move";
plot.target.trigger('jqplotDragStart', [neighbor.seriesIndex, neighbor.pointIndex, gridpos, datapos]);
}
}
// Just in case of a hickup, we'll clear the drag canvas and reset.
else {
var ctx = dc._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
dc.isDragging = false;
}
}
function handleUp(ev, gridpos, datapos, neighbor, plot) {
if (plot.plugins.dragable.dragCanvas.isDragging) {
var dc = plot.plugins.dragable.dragCanvas;
// clear the canvas
var ctx = dc._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
dc.isDragging = false;
// redraw the series canvas at the new point.
var dp = dc._neighbor;
var s = plot.series[dp.seriesIndex];
var drag = s.plugins.dragable;
// compute the new grid position with any constraints.
var x = (drag.constrainTo == 'y') ? dp.data[0] : datapos[s.xaxis];
var y = (drag.constrainTo == 'x') ? dp.data[1] : datapos[s.yaxis];
// var x = datapos[s.xaxis];
// var y = datapos[s.yaxis];
s.data[dp.pointIndex][0] = x;
s.data[dp.pointIndex][1] = y;
plot.drawSeries({preventJqPlotSeriesDrawTrigger:true}, dp.seriesIndex);
dc._neighbor = null;
ev.target.style.cursor = dc._cursors.pop();
plot.target.trigger('jqplotDragStop', [gridpos, datapos]);
}
}
})(jQuery);

View File

@@ -0,0 +1,305 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// class $.jqplot.EnhancedLegendRenderer
// Legend renderer which can specify the number of rows and/or columns in the legend.
$.jqplot.EnhancedLegendRenderer = function(){
$.jqplot.TableLegendRenderer.call(this);
};
$.jqplot.EnhancedLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
$.jqplot.EnhancedLegendRenderer.prototype.constructor = $.jqplot.EnhancedLegendRenderer;
// called with scope of legend.
$.jqplot.EnhancedLegendRenderer.prototype.init = function(options) {
// prop: numberRows
// Maximum number of rows in the legend. 0 or null for unlimited.
this.numberRows = null;
// prop: numberColumns
// Maximum number of columns in the legend. 0 or null for unlimited.
this.numberColumns = null;
// prop: seriesToggle
// false to not enable series on/off toggling on the legend.
// true or a fadein/fadeout speed (number of milliseconds or 'fast', 'normal', 'slow')
// to enable show/hide of series on click of legend item.
this.seriesToggle = 'normal';
// prop: seriesToggleReplot
// True to replot the chart after toggling series on/off.
// This will set the series show property to false.
// This allows for rescaling or other maniplation of chart.
// Set to an options object (e.g. {resetAxes: true}) for replot options.
this.seriesToggleReplot = false;
// prop: disableIEFading
// true to toggle series with a show/hide method only and not allow fading in/out.
// This is to overcome poor performance of fade in some versions of IE.
this.disableIEFading = true;
$.extend(true, this, options);
if (this.seriesToggle) {
$.jqplot.postDrawHooks.push(postDraw);
}
};
// called with scope of legend
$.jqplot.EnhancedLegendRenderer.prototype.draw = function(offsets, plot) {
var legend = this;
if (this.show) {
var series = this._series;
var s;
var ss = 'position:absolute;';
ss += (this.background) ? 'background:'+this.background+';' : '';
ss += (this.border) ? 'border:'+this.border+';' : '';
ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
if (this.seriesToggle) {
this._elem.css('z-index', '3');
}
var pad = false,
reverse = false,
nr, nc;
if (this.numberRows) {
nr = this.numberRows;
if (!this.numberColumns){
nc = Math.ceil(series.length/nr);
}
else{
nc = this.numberColumns;
}
}
else if (this.numberColumns) {
nc = this.numberColumns;
nr = Math.ceil(series.length/this.numberColumns);
}
else {
nr = series.length;
nc = 1;
}
var i, j, tr, td1, td2, lt, rs, div, div0, div1;
var idx = 0;
// check to see if we need to reverse
for (i=series.length-1; i>=0; i--) {
if (nc == 1 && series[i]._stack || series[i].renderer.constructor == $.jqplot.BezierCurveRenderer){
reverse = true;
}
}
for (i=0; i<nr; i++) {
tr = $(document.createElement('tr'));
tr.addClass('jqplot-table-legend');
if (reverse){
tr.prependTo(this._elem);
}
else{
tr.appendTo(this._elem);
}
for (j=0; j<nc; j++) {
if (idx < series.length && (series[idx].show || series[idx].showLabel)){
s = series[idx];
lt = this.labels[idx] || s.label.toString();
if (lt) {
var color = s.color;
if (!reverse){
if (i>0){
pad = true;
}
else{
pad = false;
}
}
else{
if (i == nr -1){
pad = false;
}
else{
pad = true;
}
}
rs = (pad) ? this.rowSpacing : '0';
td1 = $(document.createElement('td'));
td1.addClass('jqplot-table-legend jqplot-table-legend-swatch');
td1.css({textAlign: 'center', paddingTop: rs});
div0 = $(document.createElement('div'));
div0.addClass('jqplot-table-legend-swatch-outline');
div1 = $(document.createElement('div'));
div1.addClass('jqplot-table-legend-swatch');
div1.css({backgroundColor: color, borderColor: color});
td1.append(div0.append(div1));
td2 = $(document.createElement('td'));
td2.addClass('jqplot-table-legend jqplot-table-legend-label');
td2.css('paddingTop', rs);
// td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
// '<div><div class="jqplot-table-legend-swatch" style="background-color:'+color+';border-color:'+color+';"></div>'+
// '</div></td>');
// td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
if (this.escapeHtml){
td2.text(lt);
}
else {
td2.html(lt);
}
if (reverse) {
if (this.showLabels) {td2.prependTo(tr);}
if (this.showSwatches) {td1.prependTo(tr);}
}
else {
if (this.showSwatches) {td1.appendTo(tr);}
if (this.showLabels) {td2.appendTo(tr);}
}
if (this.seriesToggle) {
// add an overlay for clicking series on/off
// div0 = $(document.createElement('div'));
// div0.addClass('jqplot-table-legend-overlay');
// div0.css({position:'relative', left:0, top:0, height:'100%', width:'100%'});
// tr.append(div0);
var speed;
if (typeof(this.seriesToggle) === 'string' || typeof(this.seriesToggle) === 'number') {
if (!$.jqplot.use_excanvas || !this.disableIEFading) {
speed = this.seriesToggle;
}
}
if (this.showSwatches) {
td1.bind('click', {series:s, speed:speed, plot: plot, replot:this.seriesToggleReplot}, handleToggle);
td1.addClass('jqplot-seriesToggle');
}
if (this.showLabels) {
td2.bind('click', {series:s, speed:speed, plot: plot, replot:this.seriesToggleReplot}, handleToggle);
td2.addClass('jqplot-seriesToggle');
}
// for series that are already hidden, add the hidden class
if (!s.show && s.showLabel) {
td1.addClass('jqplot-series-hidden');
td2.addClass('jqplot-series-hidden');
}
}
pad = true;
}
}
idx++;
}
td1 = td2 = div0 = div1 = null;
}
}
return this._elem;
};
var handleToggle = function (ev) {
var d = ev.data,
s = d.series,
replot = d.replot,
plot = d.plot,
speed = d.speed,
sidx = s.index,
showing = false;
if (s.canvas._elem.is(':hidden') || !s.show) {
showing = true;
}
var doLegendToggle = function() {
if (replot) {
var opts = {};
if ($.isPlainObject(replot)) {
$.extend(true, opts, replot);
}
plot.replot(opts);
// if showing, there was no canvas element to fade in, so hide here
// and then do a fade in.
if (showing && speed) {
var s = plot.series[sidx];
if (s.shadowCanvas._elem) {
s.shadowCanvas._elem.hide().fadeIn(speed);
}
s.canvas._elem.hide().fadeIn(speed);
s.canvas._elem.nextAll('.jqplot-point-label.jqplot-series-'+s.index).hide().fadeIn(speed);
}
}
else {
var s = plot.series[sidx];
if (s.canvas._elem.is(':hidden') || !s.show) {
// Not sure if there is a better way to check for showSwatches and showLabels === true.
// Test for "undefined" since default values for both showSwatches and showLables is true.
if (typeof plot.options.legend.showSwatches === 'undefined' || plot.options.legend.showSwatches === true) {
plot.legend._elem.find('td').eq(sidx * 2).addClass('jqplot-series-hidden');
}
if (typeof plot.options.legend.showLabels === 'undefined' || plot.options.legend.showLabels === true) {
plot.legend._elem.find('td').eq((sidx * 2) + 1).addClass('jqplot-series-hidden');
}
}
else {
if (typeof plot.options.legend.showSwatches === 'undefined' || plot.options.legend.showSwatches === true) {
plot.legend._elem.find('td').eq(sidx * 2).removeClass('jqplot-series-hidden');
}
if (typeof plot.options.legend.showLabels === 'undefined' || plot.options.legend.showLabels === true) {
plot.legend._elem.find('td').eq((sidx * 2) + 1).removeClass('jqplot-series-hidden');
}
}
}
};
s.toggleDisplay(ev, doLegendToggle);
};
// called with scope of plot.
var postDraw = function () {
if (this.legend.renderer.constructor == $.jqplot.EnhancedLegendRenderer && this.legend.seriesToggle){
var e = this.legend._elem.detach();
this.eventCanvas._elem.after(e);
}
};
})(jQuery);

View File

@@ -0,0 +1,261 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// class $.jqplot.EnhancedPieLegendRenderer
// Legend renderer which can specify the number of rows and/or columns in the legend
// Similar to EnhancedLegendRenderer, but for pie charts
$.jqplot.EnhancedPieLegendRenderer = function(){
$.jqplot.TableLegendRenderer.call(this);
};
$.jqplot.EnhancedPieLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
$.jqplot.EnhancedPieLegendRenderer.prototype.constructor = $.jqplot.EnhancedPieLegendRenderer;
// called with scope of legend.
$.jqplot.EnhancedPieLegendRenderer.prototype.init = function(options) {
// prop: numberRows
// Maximum number of rows in the legend. 0 or null for unlimited.
this.numberRows = null;
// prop: numberColumns
// Maximum number of columns in the legend. 0 or null for unlimited.
this.numberColumns = null;
// prop: seriesToggle
// false to not enable series on/off toggling on the legend.
// true or a fadein/fadeout speed (number of milliseconds or 'fast', 'normal', 'slow')
// to enable show/hide of series on click of legend item.
this.seriesToggle = 'normal';
// prop: seriesToggleReplot
// True to replot the chart after toggling series on/off.
// This will set the series show property to false.
// This allows for rescaling or other maniplation of chart.
// Set to an options object (e.g. {resetAxes: true}) for replot options.
this.seriesToggleReplot = false;
// prop: disableIEFading
// true to toggle series with a show/hide method only and not allow fading in/out.
// This is to overcome poor performance of fade in some versions of IE.
this.disableIEFading = true;
// prop: toolTips
// optional array of toolTip text corresponding to each pie slice
this.toolTips = [];
$.extend(true, this, options);
if (this.seriesToggle) {
$.jqplot.postDrawHooks.push(postDraw);
}
};
// called with scope of legend
$.jqplot.EnhancedPieLegendRenderer.prototype.draw = function(offsets, plot) {
var legend = this;
if (this.show) {
var series = this._series;
var s;
var ss = 'position:absolute;';
ss += (this.background) ? 'background:'+this.background+';' : '';
ss += (this.border) ? 'border:'+this.border+';' : '';
ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
if (this.seriesToggle) {
this._elem.css('z-index', '3');
}
var pad = false,
reverse = false,
nr, nc;
var s = series[0];
var slen = s.data.length;
var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
if (this.numberRows) {
nr = this.numberRows;
if (!this.numberColumns){
nc = Math.ceil(slen/nr);
}
else{
nc = this.numberColumns;
}
}
else if (this.numberColumns) {
nc = this.numberColumns;
nr = Math.ceil(slen/this.numberColumns);
}
else {
nr = slen;
nc = 1;
}
var i, j, tr, td1, td2, lt, rs, div, div0, div1;
var idx = 0;
// check to see if we need to reverse
for (i=series.length-1; i>=0; i--) {
if (nc == 1 && series[i]._stack || series[i].renderer.constructor == $.jqplot.BezierCurveRenderer){
reverse = true;
}
}
for (i=0; i<nr; i++) {
tr = $(document.createElement('tr'));
tr.addClass('jqplot-table-legend');
if (reverse){
tr.prependTo(this._elem);
}
else{
tr.appendTo(this._elem);
}
for (j=0; j<nc; j++) {
if (idx < slen){
lt = this.labels[idx] || s.data[idx][0].toString();
tt = this.toolTips[idx];
if (lt) {
var color = colorGenerator.next();
if (!reverse){
if (i>0){
pad = true;
}
else{
pad = false;
}
}
else{
if (i == nr -1){
pad = false;
}
else{
pad = true;
}
}
rs = (pad) ? this.rowSpacing : '0';
td1 = $(document.createElement('td'));
td1.addClass('jqplot-table-legend jqplot-table-legend-swatch');
td1.css({textAlign: 'center', paddingTop: rs});
div0 = $(document.createElement('div'));
div0.addClass('jqplot-table-legend-swatch-outline');
if (tt !== undefined) {
div0.attr("title", tt);
}
div1 = $(document.createElement('div'));
div1.addClass('jqplot-table-legend-swatch');
div1.css({backgroundColor: color, borderColor: color});
td1.append(div0.append(div1));
td2 = $(document.createElement('td'));
td2.addClass('jqplot-table-legend jqplot-table-legend-label');
td2.css('paddingTop', rs);
if (tt !== undefined) {
td2.attr("title", tt);
}
if (this.escapeHtml){
td2.text(lt);
}
else {
td2.html(lt);
}
if (reverse) {
if (this.showLabels) {td2.prependTo(tr);}
if (this.showSwatches) {td1.prependTo(tr);}
}
else {
if (this.showSwatches) {td1.appendTo(tr);}
if (this.showLabels) {td2.appendTo(tr);}
}
if (this.seriesToggle) {
var speed;
if (typeof(this.seriesToggle) === 'string' || typeof(this.seriesToggle) === 'number') {
if (!$.jqplot.use_excanvas || !this.disableIEFading) {
speed = this.seriesToggle;
}
}
if (this.showSwatches) {
td1.bind('click', {series:s, index:idx, speed:speed, plot: plot, replot:this.seriesToggleReplot}, handleToggle);
td1.addClass('jqplot-seriesToggle');
}
if (this.showLabels) {
td2.bind('click', {series:s, index:idx, speed:speed, plot: plot, replot:this.seriesToggleReplot}, handleToggle);
td2.addClass('jqplot-seriesToggle');
}
// for slices that are already hidden, add the hidden class
if (s.showSlice[idx] === false && s.showLabel) {
td1.addClass('jqplot-series-hidden');
td2.addClass('jqplot-series-hidden');
}
}
pad = true;
}
}
idx++;
}
td1 = td2 = div0 = div1 = null;
}
}
return this._elem;
};
var handleToggle = function (ev) {
var d = ev.data,
replot = d.replot,
plot = d.plot,
idx = d.index;
d.series.showSlice[idx] = (d.series.showSlice[idx] === false) ? true : false;
var opts = {};
if ($.isPlainObject(replot)) {
$.extend(true, opts, replot);
}
plot.replot(opts);
};
// called with scope of plot.
var postDraw = function () {
if (this.legend.renderer.constructor == $.jqplot.EnhancedPieLegendRenderer && this.legend.seriesToggle) {
var e = this.legend._elem.detach();
this.eventCanvas._elem.after(e);
}
};
})(jQuery);

View File

@@ -0,0 +1,943 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.FunnelRenderer
* Plugin renderer to draw a funnel chart.
* x values, if present, will be used as labels.
* y values give area size.
*
* Funnel charts will draw a single series
* only.
*
* To use this renderer, you need to include the
* funnel renderer plugin, for example:
*
* > <script type="text/javascript" src="plugins/jqplot.funnelRenderer.js"></script>
*
* Properties described here are passed into the $.jqplot function
* as options on the series renderer. For example:
*
* > plot2 = $.jqplot('chart2', [s1, s2], {
* > seriesDefaults: {
* > renderer:$.jqplot.FunnelRenderer,
* > rendererOptions:{
* > sectionMargin: 12,
* > widthRatio: 0.3
* > }
* > }
* > });
*
* IMPORTANT
*
* *The funnel renderer will reorder data in descending order* so the largest value in
* the data set is first and displayed on top of the funnel. Data will then
* be displayed in descending order down the funnel. The area of each funnel
* section will correspond to the value of each data point relative to the sum
* of all values. That is section area is proportional to section value divided by
* sum of all section values.
*
* If your data is not in descending order when passed into the plot, *it will be
* reordered* when stored in the series.data property. A copy of the unordered
* data is kept in the series._unorderedData property.
*
* A funnel plot will trigger events on the plot target
* according to user interaction. All events return the event object,
* the series index, the point (section) index, and the point data for
* the appropriate section. *Note* the point index will referr to the ordered
* data, not the original unordered data.
*
* 'jqplotDataMouseOver' - triggered when mousing over a section.
* 'jqplotDataHighlight' - triggered the first time user mouses over a section,
* if highlighting is enabled.
* 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
* a highlighted section.
* 'jqplotDataClick' - triggered when the user clicks on a section.
* 'jqplotDataRightClick' - tiggered when the user right clicks on a section if
* the "captureRightClick" option is set to true on the plot.
*/
$.jqplot.FunnelRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.FunnelRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.FunnelRenderer.prototype.constructor = $.jqplot.FunnelRenderer;
// called with scope of a series
$.jqplot.FunnelRenderer.prototype.init = function(options, plot) {
// Group: Properties
//
// prop: padding
// padding between the funnel and plot edges, legend, etc.
this.padding = {top: 20, right: 20, bottom: 20, left: 20};
// prop: sectionMargin
// spacing between funnel sections in pixels.
this.sectionMargin = 6;
// prop: fill
// true or false, whether to fill the areas.
this.fill = true;
// prop: shadowOffset
// offset of the shadow from the area and offset of
// each succesive stroke of the shadow from the last.
this.shadowOffset = 2;
// prop: shadowAlpha
// transparency of the shadow (0 = transparent, 1 = opaque)
this.shadowAlpha = 0.07;
// prop: shadowDepth
// number of strokes to apply to the shadow,
// each stroke offset shadowOffset from the last.
this.shadowDepth = 5;
// prop: highlightMouseOver
// True to highlight area when moused over.
// This must be false to enable highlightMouseDown to highlight when clicking on a area.
this.highlightMouseOver = true;
// prop: highlightMouseDown
// True to highlight when a mouse button is pressed over a area.
// This will be disabled if highlightMouseOver is true.
this.highlightMouseDown = false;
// prop: highlightColors
// array of colors to use when highlighting an area.
this.highlightColors = [];
// prop: widthRatio
// The ratio of the width of the top of the funnel to the bottom.
// a ratio of 0 will make an upside down pyramid.
this.widthRatio = 0.2;
// prop: lineWidth
// width of line if areas are stroked and not filled.
this.lineWidth = 2;
// prop: dataLabels
// Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
// Defaults to percentage of each pie slice.
this.dataLabels = 'percent';
// prop: showDataLabels
// true to show data labels on slices.
this.showDataLabels = false;
// prop: dataLabelFormatString
// Format string for data labels. If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
this.dataLabelFormatString = null;
// prop: dataLabelThreshold
// Threshhold in percentage (0 - 100) of pie area, below which no label will be displayed.
// This applies to all label types, not just to percentage labels.
this.dataLabelThreshold = 3;
this._type = 'funnel';
this.tickRenderer = $.jqplot.FunnelTickRenderer;
// if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
if (options.highlightMouseDown && options.highlightMouseOver == null) {
options.highlightMouseOver = false;
}
$.extend(true, this, options);
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// lengths of bases, or horizontal sides of areas of trapezoid.
this._bases = [];
// total area
this._atot;
// areas of segments.
this._areas = [];
// vertical lengths of segments.
this._lengths = [];
// angle of the funnel to vertical.
this._angle;
this._dataIndices = [];
// sort data
this._unorderedData = $.extend(true, [], this.data);
var idxs = $.extend(true, [], this.data);
for (var i=0; i<idxs.length; i++) {
idxs[i].push(i);
}
this.data.sort( function (a, b) { return b[1] - a[1]; } );
idxs.sort( function (a, b) { return b[1] - a[1]; });
for (var i=0; i<idxs.length; i++) {
this._dataIndices.push(idxs[i][2]);
}
// set highlight colors if none provided
if (this.highlightColors.length == 0) {
for (var i=0; i<this.seriesColors.length; i++){
var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var sum = newrgb[0] + newrgb[1] + newrgb[2];
for (var j=0; j<3; j++) {
// when darkening, lowest color component can be is 60.
newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.4 * (255 - newrgb[j]);
newrgb[j] = parseInt(newrgb[j], 10);
}
this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
}
}
plot.postParseOptionsHooks.addOnce(postParseOptions);
plot.postInitHooks.addOnce(postInit);
plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
plot.postDrawHooks.addOnce(postPlotDraw);
};
// gridData will be of form [label, percentage of total]
$.jqplot.FunnelRenderer.prototype.setGridData = function(plot) {
// set gridData property. This will hold angle in radians of each data point.
var sum = 0;
var td = [];
for (var i=0; i<this.data.length; i++){
sum += this.data[i][1];
td.push([this.data[i][0], this.data[i][1]]);
}
// normalize y values, so areas are proportional.
for (var i=0; i<td.length; i++) {
td[i][1] = td[i][1]/sum;
}
this._bases = new Array(td.length + 1);
this._lengths = new Array(td.length);
this.gridData = td;
};
$.jqplot.FunnelRenderer.prototype.makeGridData = function(data, plot) {
// set gridData property. This will hold angle in radians of each data point.
var sum = 0;
var td = [];
for (var i=0; i<this.data.length; i++){
sum += this.data[i][1];
td.push([this.data[i][0], this.data[i][1]]);
}
// normalize y values, so areas are proportional.
for (var i=0; i<td.length; i++) {
td[i][1] = td[i][1]/sum;
}
this._bases = new Array(td.length + 1);
this._lengths = new Array(td.length);
return td;
};
$.jqplot.FunnelRenderer.prototype.drawSection = function (ctx, vertices, color, isShadow) {
var fill = this.fill;
var lineWidth = this.lineWidth;
ctx.save();
if (isShadow) {
for (var i=0; i<this.shadowDepth; i++) {
ctx.save();
ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
doDraw();
}
}
else {
doDraw();
}
function doDraw () {
ctx.beginPath();
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.moveTo(vertices[0][0], vertices[0][1]);
for (var i=1; i<4; i++) {
ctx.lineTo(vertices[i][0], vertices[i][1]);
}
ctx.closePath();
if (fill) {
ctx.fill();
}
else {
ctx.stroke();
}
}
if (isShadow) {
for (var i=0; i<this.shadowDepth; i++) {
ctx.restore();
}
}
ctx.restore();
};
// called with scope of series
$.jqplot.FunnelRenderer.prototype.draw = function (ctx, gd, options, plot) {
var i;
var opts = (options != undefined) ? options : {};
// offset and direction of offset due to legend placement
var offx = 0;
var offy = 0;
var trans = 1;
this._areas = [];
// var colorGenerator = new this.colorGenerator(this.seriesColors);
if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
var li = options.legendInfo;
switch (li.location) {
case 'nw':
offx = li.width + li.xoffset;
break;
case 'w':
offx = li.width + li.xoffset;
break;
case 'sw':
offx = li.width + li.xoffset;
break;
case 'ne':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'e':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'se':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'n':
offy = li.height + li.yoffset;
break;
case 's':
offy = li.height + li.yoffset;
trans = -1;
break;
default:
break;
}
}
var loff = (trans==1) ? this.padding.left + offx : this.padding.left;
var toff = (trans==1) ? this.padding.top + offy : this.padding.top;
var roff = (trans==-1) ? this.padding.right + offx : this.padding.right;
var boff = (trans==-1) ? this.padding.bottom + offy : this.padding.bottom;
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
var cw = ctx.canvas.width;
var ch = ctx.canvas.height;
this._bases[0] = cw - loff - roff;
var ltot = this._length = ch - toff - boff;
var hend = this._bases[0]*this.widthRatio;
this._atot = ltot/2 * (this._bases[0] + this._bases[0]*this.widthRatio);
this._angle = Math.atan((this._bases[0] - hend)/2/ltot);
for (i=0; i<gd.length; i++) {
this._areas.push(gd[i][1] * this._atot);
}
var guess, err, count, lsum=0;
var tolerance = 0.0001;
for (i=0; i<this._areas.length; i++) {
guess = this._areas[i]/this._bases[i];
err = 999999;
this._lengths[i] = guess;
count = 0;
while (err > this._lengths[i]*tolerance && count < 100) {
this._lengths[i] = this._areas[i]/(this._bases[i] - this._lengths[i] * Math.tan(this._angle));
err = Math.abs(this._lengths[i] - guess);
this._bases[i+1] = this._bases[i] - (2*this._lengths[i]*Math.tan(this._angle));
guess = this._lengths[i];
count++;
}
lsum += this._lengths[i];
}
// figure out vertices of each section
this._vertices = new Array(gd.length);
// these are 4 coners of entire trapezoid
var p0 = [loff, toff],
p1 = [loff+this._bases[0], toff],
p2 = [loff + (this._bases[0] - this._bases[this._bases.length-1])/2, toff + this._length],
p3 = [p2[0] + this._bases[this._bases.length-1], p2[1]];
// equations of right and left sides, returns x, y values given height of section (y value)
function findleft (l) {
var m = (p0[1] - p2[1])/(p0[0] - p2[0]);
var b = p0[1] - m*p0[0];
var y = l + p0[1];
return [(y - b)/m, y];
}
function findright (l) {
var m = (p1[1] - p3[1])/(p1[0] - p3[0]);
var b = p1[1] - m*p1[0];
var y = l + p1[1];
return [(y - b)/m, y];
}
var x = offx, y = offy;
var h=0, adj=0;
for (i=0; i<gd.length; i++) {
this._vertices[i] = new Array();
var v = this._vertices[i];
var sm = this.sectionMargin;
if (i == 0) {
adj = 0;
}
if (i == 1) {
adj = sm/3;
}
else if (i > 0 && i < gd.length-1) {
adj = sm/2;
}
else if (i == gd.length -1) {
adj = 2*sm/3;
}
v.push(findleft(h+adj));
v.push(findright(h+adj));
h += this._lengths[i];
if (i == 0) {
adj = -2*sm/3;
}
else if (i > 0 && i < gd.length-1) {
adj = -sm/2;
}
else if (i == gd.length - 1) {
adj = 0;
}
v.push(findright(h+adj));
v.push(findleft(h+adj));
}
if (this.shadow) {
var shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
for (var i=0; i<gd.length; i++) {
this.renderer.drawSection.call (this, ctx, this._vertices[i], shadowColor, true);
}
}
for (var i=0; i<gd.length; i++) {
var v = this._vertices[i];
this.renderer.drawSection.call (this, ctx, v, this.seriesColors[i]);
if (this.showDataLabels && gd[i][1]*100 >= this.dataLabelThreshold) {
var fstr, label;
if (this.dataLabels == 'label') {
fstr = this.dataLabelFormatString || '%s';
label = $.jqplot.sprintf(fstr, gd[i][0]);
}
else if (this.dataLabels == 'value') {
fstr = this.dataLabelFormatString || '%d';
label = $.jqplot.sprintf(fstr, this.data[i][1]);
}
else if (this.dataLabels == 'percent') {
fstr = this.dataLabelFormatString || '%d%%';
label = $.jqplot.sprintf(fstr, gd[i][1]*100);
}
else if (this.dataLabels.constructor == Array) {
fstr = this.dataLabelFormatString || '%s';
label = $.jqplot.sprintf(fstr, this.dataLabels[this._dataIndices[i]]);
}
var fact = (this._radius ) * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
var x = (v[0][0] + v[1][0])/2 + this.canvas._offsets.left;
var y = (v[1][1] + v[2][1])/2 + this.canvas._offsets.top;
var labelelem = $('<span class="jqplot-funnel-series jqplot-data-label" style="position:absolute;">' + label + '</span>').insertBefore(plot.eventCanvas._elem);
x -= labelelem.width()/2;
y -= labelelem.height()/2;
x = Math.round(x);
y = Math.round(y);
labelelem.css({left: x, top: y});
}
}
};
$.jqplot.FunnelAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
};
$.jqplot.FunnelAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.FunnelAxisRenderer.prototype.constructor = $.jqplot.FunnelAxisRenderer;
// There are no traditional axes on a funnel chart. We just need to provide
// dummy objects with properties so the plot will render.
// called with scope of axis object.
$.jqplot.FunnelAxisRenderer.prototype.init = function(options){
//
this.tickRenderer = $.jqplot.FunnelTickRenderer;
$.extend(true, this, options);
// I don't think I'm going to need _dataBounds here.
// have to go Axis scaling in a way to fit chart onto plot area
// and provide u2p and p2u functionality for mouse cursor, etc.
// for convienence set _dataBounds to 0 and 100 and
// set min/max to 0 and 100.
this._dataBounds = {min:0, max:100};
this.min = 0;
this.max = 100;
this.showTicks = false;
this.ticks = [];
this.showMark = false;
this.show = false;
};
/**
* Class: $.jqplot.FunnelLegendRenderer
* Legend Renderer specific to funnel plots. Set by default
* when the user creates a funnel plot.
*/
$.jqplot.FunnelLegendRenderer = function(){
$.jqplot.TableLegendRenderer.call(this);
};
$.jqplot.FunnelLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
$.jqplot.FunnelLegendRenderer.prototype.constructor = $.jqplot.FunnelLegendRenderer;
$.jqplot.FunnelLegendRenderer.prototype.init = function(options) {
// Group: Properties
//
// prop: numberRows
// Maximum number of rows in the legend. 0 or null for unlimited.
this.numberRows = null;
// prop: numberColumns
// Maximum number of columns in the legend. 0 or null for unlimited.
this.numberColumns = null;
$.extend(true, this, options);
};
// called with context of legend
$.jqplot.FunnelLegendRenderer.prototype.draw = function() {
var legend = this;
if (this.show) {
var series = this._series;
var ss = 'position:absolute;';
ss += (this.background) ? 'background:'+this.background+';' : '';
ss += (this.border) ? 'border:'+this.border+';' : '';
ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
ss += (this.marginTop != null) ? 'margin-top:'+this.marginTop+';' : '';
ss += (this.marginBottom != null) ? 'margin-bottom:'+this.marginBottom+';' : '';
ss += (this.marginLeft != null) ? 'margin-left:'+this.marginLeft+';' : '';
ss += (this.marginRight != null) ? 'margin-right:'+this.marginRight+';' : '';
this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
// Funnel charts legends don't go by number of series, but by number of data points
// in the series. Refactor things here for that.
var pad = false,
reverse = false,
nr, nc;
var s = series[0];
var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
if (s.show) {
var pd = s.data;
if (this.numberRows) {
nr = this.numberRows;
if (!this.numberColumns){
nc = Math.ceil(pd.length/nr);
}
else{
nc = this.numberColumns;
}
}
else if (this.numberColumns) {
nc = this.numberColumns;
nr = Math.ceil(pd.length/this.numberColumns);
}
else {
nr = pd.length;
nc = 1;
}
var i, j, tr, td1, td2, lt, rs, color;
var idx = 0;
for (i=0; i<nr; i++) {
if (reverse){
tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
}
else{
tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
}
for (j=0; j<nc; j++) {
if (idx < pd.length){
lt = this.labels[idx] || pd[idx][0].toString();
color = colorGenerator.next();
if (!reverse){
if (i>0){
pad = true;
}
else{
pad = false;
}
}
else{
if (i == nr -1){
pad = false;
}
else{
pad = true;
}
}
rs = (pad) ? this.rowSpacing : '0';
td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
'<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
'</div></td>');
td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
if (this.escapeHtml){
td2.text(lt);
}
else {
td2.html(lt);
}
if (reverse) {
td2.prependTo(tr);
td1.prependTo(tr);
}
else {
td1.appendTo(tr);
td2.appendTo(tr);
}
pad = true;
}
idx++;
}
}
}
}
return this._elem;
};
// $.jqplot.FunnelLegendRenderer.prototype.pack = function(offsets) {
// if (this.show) {
// // fake a grid for positioning
// var grid = {_top:offsets.top, _left:offsets.left, _right:offsets.right, _bottom:this._plotDimensions.height - offsets.bottom};
// if (this.placement == 'insideGrid') {
// switch (this.location) {
// case 'nw':
// var a = grid._left + this.xoffset;
// var b = grid._top + this.yoffset;
// this._elem.css('left', a);
// this._elem.css('top', b);
// break;
// case 'n':
// var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
// var b = grid._top + this.yoffset;
// this._elem.css('left', a);
// this._elem.css('top', b);
// break;
// case 'ne':
// var a = offsets.right + this.xoffset;
// var b = grid._top + this.yoffset;
// this._elem.css({right:a, top:b});
// break;
// case 'e':
// var a = offsets.right + this.xoffset;
// var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
// this._elem.css({right:a, top:b});
// break;
// case 'se':
// var a = offsets.right + this.xoffset;
// var b = offsets.bottom + this.yoffset;
// this._elem.css({right:a, bottom:b});
// break;
// case 's':
// var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
// var b = offsets.bottom + this.yoffset;
// this._elem.css({left:a, bottom:b});
// break;
// case 'sw':
// var a = grid._left + this.xoffset;
// var b = offsets.bottom + this.yoffset;
// this._elem.css({left:a, bottom:b});
// break;
// case 'w':
// var a = grid._left + this.xoffset;
// var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
// this._elem.css({left:a, top:b});
// break;
// default: // same as 'se'
// var a = grid._right - this.xoffset;
// var b = grid._bottom + this.yoffset;
// this._elem.css({right:a, bottom:b});
// break;
// }
//
// }
// else {
// switch (this.location) {
// case 'nw':
// var a = this._plotDimensions.width - grid._left + this.xoffset;
// var b = grid._top + this.yoffset;
// this._elem.css('right', a);
// this._elem.css('top', b);
// break;
// case 'n':
// var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
// var b = this._plotDimensions.height - grid._top + this.yoffset;
// this._elem.css('left', a);
// this._elem.css('bottom', b);
// break;
// case 'ne':
// var a = this._plotDimensions.width - offsets.right + this.xoffset;
// var b = grid._top + this.yoffset;
// this._elem.css({left:a, top:b});
// break;
// case 'e':
// var a = this._plotDimensions.width - offsets.right + this.xoffset;
// var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
// this._elem.css({left:a, top:b});
// break;
// case 'se':
// var a = this._plotDimensions.width - offsets.right + this.xoffset;
// var b = offsets.bottom + this.yoffset;
// this._elem.css({left:a, bottom:b});
// break;
// case 's':
// var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
// var b = this._plotDimensions.height - offsets.bottom + this.yoffset;
// this._elem.css({left:a, top:b});
// break;
// case 'sw':
// var a = this._plotDimensions.width - grid._left + this.xoffset;
// var b = offsets.bottom + this.yoffset;
// this._elem.css({right:a, bottom:b});
// break;
// case 'w':
// var a = this._plotDimensions.width - grid._left + this.xoffset;
// var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
// this._elem.css({right:a, top:b});
// break;
// default: // same as 'se'
// var a = grid._right - this.xoffset;
// var b = grid._bottom + this.yoffset;
// this._elem.css({right:a, bottom:b});
// break;
// }
// }
// }
// };
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = options.axesDefaults || {};
options.legend = options.legend || {};
options.seriesDefaults = options.seriesDefaults || {};
// only set these if there is a funnel series
var setopts = false;
if (options.seriesDefaults.renderer == $.jqplot.FunnelRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer == $.jqplot.FunnelRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.FunnelAxisRenderer;
options.legend.renderer = $.jqplot.FunnelLegendRenderer;
options.legend.preDraw = true;
options.sortData = false;
options.seriesDefaults.pointLabels = {show: false};
}
}
function postInit(target, data, options) {
// if multiple series, add a reference to the previous one so that
// funnel rings can nest.
for (var i=0; i<this.series.length; i++) {
if (this.series[i].renderer.constructor == $.jqplot.FunnelRenderer) {
// don't allow mouseover and mousedown at same time.
if (this.series[i].highlightMouseOver) {
this.series[i].highlightMouseDown = false;
}
}
}
}
// called with scope of plot
function postParseOptions(options) {
for (var i=0; i<this.series.length; i++) {
this.series[i].seriesColors = this.seriesColors;
this.series[i].colorGenerator = $.jqplot.colorGenerator;
}
}
function highlight (plot, sidx, pidx) {
var s = plot.series[sidx];
var canvas = plot.plugins.funnelRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
s._highlightedPoint = pidx;
plot.plugins.funnelRenderer.highlightedSeriesIndex = sidx;
s.renderer.drawSection.call(s, canvas._ctx, s._vertices[pidx], s.highlightColors[pidx], false);
}
function unhighlight (plot) {
var canvas = plot.plugins.funnelRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
for (var i=0; i<plot.series.length; i++) {
plot.series[i]._highlightedPoint = null;
}
plot.plugins.funnelRenderer.highlightedSeriesIndex = null;
plot.target.trigger('jqplotDataUnhighlight');
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt1 = jQuery.Event('jqplotDataMouseOver');
evt1.pageX = ev.pageX;
evt1.pageY = ev.pageY;
plot.target.trigger(evt1, ins);
if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.funnelRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.funnelRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
}
function handleClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt = jQuery.Event('jqplotDataClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var idx = plot.plugins.funnelRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
var evt = jQuery.Event('jqplotDataRightClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
function postPlotDraw() {
// Memory Leaks patch
if (this.plugins.funnelRenderer && this.plugins.funnelRenderer.highlightCanvas) {
this.plugins.funnelRenderer.highlightCanvas.resetCanvas();
this.plugins.funnelRenderer.highlightCanvas = null;
}
this.plugins.funnelRenderer = {};
this.plugins.funnelRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
// do we have any data labels? if so, put highlight canvas before those
var labels = $(this.targetId+' .jqplot-data-label');
if (labels.length) {
$(labels[0]).before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
}
// else put highlight canvas before event canvas.
else {
this.eventCanvas._elem.before(this.plugins.funnelRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-funnelRenderer-highlight-canvas', this._plotDimensions, this));
}
var hctx = this.plugins.funnelRenderer.highlightCanvas.setContext();
this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
}
$.jqplot.preInitHooks.push(preInit);
$.jqplot.FunnelTickRenderer = function() {
$.jqplot.AxisTickRenderer.call(this);
};
$.jqplot.FunnelTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
$.jqplot.FunnelTickRenderer.prototype.constructor = $.jqplot.FunnelTickRenderer;
})(jQuery);

View File

@@ -0,0 +1,465 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
$.jqplot.eventListenerHooks.push(['jqplotMouseMove', handleMove]);
/**
* Class: $.jqplot.Highlighter
* Plugin which will highlight data points when they are moused over.
*
* To use this plugin, include the js
* file in your source:
*
* > <script type="text/javascript" src="plugins/jqplot.highlighter.js"></script>
*
* A tooltip providing information about the data point is enabled by default.
* To disable the tooltip, set "showTooltip" to false.
*
* You can control what data is displayed in the tooltip with various
* options. The "tooltipAxes" option controls whether the x, y or both
* data values are displayed.
*
* Some chart types (e.g. hi-low-close) have more than one y value per
* data point. To display the additional values in the tooltip, set the
* "yvalues" option to the desired number of y values present (3 for a hlc chart).
*
* By default, data values will be formatted with the same formatting
* specifiers as used to format the axis ticks. A custom format code
* can be supplied with the tooltipFormatString option. This will apply
* to all values in the tooltip.
*
* For more complete control, the "formatString" option can be set. This
* Allows conplete control over tooltip formatting. Values are passed to
* the format string in an order determined by the "tooltipAxes" and "yvalues"
* options. So, if you have a hi-low-close chart and you just want to display
* the hi-low-close values in the tooltip, you could set a formatString like:
*
* > highlighter: {
* > tooltipAxes: 'y',
* > yvalues: 3,
* > formatString:'<table class="jqplot-highlighter">
* > <tr><td>hi:</td><td>%s</td></tr>
* > <tr><td>low:</td><td>%s</td></tr>
* > <tr><td>close:</td><td>%s</td></tr></table>'
* > }
*
*/
$.jqplot.Highlighter = function(options) {
// Group: Properties
//
//prop: show
// true to show the highlight.
this.show = $.jqplot.config.enablePlugins;
// prop: markerRenderer
// Renderer used to draw the marker of the highlighted point.
// Renderer will assimilate attributes from the data point being highlighted,
// so no attributes need set on the renderer directly.
// Default is to turn off shadow drawing on the highlighted point.
this.markerRenderer = new $.jqplot.MarkerRenderer({shadow:false});
// prop: showMarker
// true to show the marker
this.showMarker = true;
// prop: lineWidthAdjust
// Pixels to add to the lineWidth of the highlight.
this.lineWidthAdjust = 2.5;
// prop: sizeAdjust
// Pixels to add to the overall size of the highlight.
this.sizeAdjust = 5;
// prop: showTooltip
// Show a tooltip with data point values.
this.showTooltip = true;
// prop: tooltipLocation
// Where to position tooltip, 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
this.tooltipLocation = 'nw';
// prop: fadeTooltip
// true = fade in/out tooltip, flase = show/hide tooltip
this.fadeTooltip = true;
// prop: tooltipFadeSpeed
// 'slow', 'def', 'fast', or number of milliseconds.
this.tooltipFadeSpeed = "fast";
// prop: tooltipOffset
// Pixel offset of tooltip from the highlight.
this.tooltipOffset = 2;
// prop: tooltipAxes
// Which axes to display in tooltip, 'x', 'y' or 'both', 'xy' or 'yx'
// 'both' and 'xy' are equivalent, 'yx' reverses order of labels.
this.tooltipAxes = 'both';
// prop; tooltipSeparator
// String to use to separate x and y axes in tooltip.
this.tooltipSeparator = ', ';
// prop; tooltipContentEditor
// Function used to edit/augment/replace the formatted tooltip contents.
// Called as str = tooltipContentEditor(str, seriesIndex, pointIndex)
// where str is the generated tooltip html and seriesIndex and pointIndex identify
// the data point being highlighted. Should return the html for the tooltip contents.
this.tooltipContentEditor = null;
// prop: useAxesFormatters
// Use the x and y axes formatters to format the text in the tooltip.
this.useAxesFormatters = true;
// prop: tooltipFormatString
// sprintf format string for the tooltip.
// Uses Ash Searle's javascript sprintf implementation
// found here: http://hexmen.com/blog/2007/03/printf-sprintf/
// See http://perldoc.perl.org/functions/sprintf.html for reference.
// Additional "p" and "P" format specifiers added by Chris Leonello.
this.tooltipFormatString = '%.5P';
// prop: formatString
// alternative to tooltipFormatString
// will format the whole tooltip text, populating with x, y values as
// indicated by tooltipAxes option. So, you could have a tooltip like:
// 'Date: %s, number of cats: %d' to format the whole tooltip at one go.
// If useAxesFormatters is true, values will be formatted according to
// Axes formatters and you can populate your tooltip string with
// %s placeholders.
this.formatString = null;
// prop: yvalues
// Number of y values to expect in the data point array.
// Typically this is 1. Certain plots, like OHLC, will
// have more y values in each data point array.
this.yvalues = 1;
// prop: bringSeriesToFront
// This option requires jQuery 1.4+
// True to bring the series of the highlighted point to the front
// of other series.
this.bringSeriesToFront = false;
this._tooltipElem;
this.isHighlighting = false;
this.currentNeighbor = null;
$.extend(true, this, options);
};
var locations = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
var locationIndicies = {'nw':0, 'n':1, 'ne':2, 'e':3, 'se':4, 's':5, 'sw':6, 'w':7};
var oppositeLocations = ['se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'];
// axis.renderer.tickrenderer.formatter
// called with scope of plot
$.jqplot.Highlighter.init = function (target, data, opts){
var options = opts || {};
// add a highlighter attribute to the plot
this.plugins.highlighter = new $.jqplot.Highlighter(options.highlighter);
};
// called within scope of series
$.jqplot.Highlighter.parseOptions = function (defaults, options) {
// Add a showHighlight option to the series
// and set it to true by default.
this.showHighlight = true;
};
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
$.jqplot.Highlighter.postPlotDraw = function() {
// Memory Leaks patch
if (this.plugins.highlighter && this.plugins.highlighter.highlightCanvas) {
this.plugins.highlighter.highlightCanvas.resetCanvas();
this.plugins.highlighter.highlightCanvas = null;
}
if (this.plugins.highlighter && this.plugins.highlighter._tooltipElem) {
this.plugins.highlighter._tooltipElem.emptyForce();
this.plugins.highlighter._tooltipElem = null;
}
this.plugins.highlighter.highlightCanvas = new $.jqplot.GenericCanvas();
this.eventCanvas._elem.before(this.plugins.highlighter.highlightCanvas.createElement(this._gridPadding, 'jqplot-highlight-canvas', this._plotDimensions, this));
this.plugins.highlighter.highlightCanvas.setContext();
var elem = document.createElement('div');
this.plugins.highlighter._tooltipElem = $(elem);
elem = null;
this.plugins.highlighter._tooltipElem.addClass('jqplot-highlighter-tooltip');
this.plugins.highlighter._tooltipElem.css({position:'absolute', display:'none'});
this.eventCanvas._elem.before(this.plugins.highlighter._tooltipElem);
};
$.jqplot.preInitHooks.push($.jqplot.Highlighter.init);
$.jqplot.preParseSeriesOptionsHooks.push($.jqplot.Highlighter.parseOptions);
$.jqplot.postDrawHooks.push($.jqplot.Highlighter.postPlotDraw);
function draw(plot, neighbor) {
var hl = plot.plugins.highlighter;
var s = plot.series[neighbor.seriesIndex];
var smr = s.markerRenderer;
var mr = hl.markerRenderer;
mr.style = smr.style;
mr.lineWidth = smr.lineWidth + hl.lineWidthAdjust;
mr.size = smr.size + hl.sizeAdjust;
var rgba = $.jqplot.getColorComponents(smr.color);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var alpha = (rgba[3] >= 0.6) ? rgba[3]*0.6 : rgba[3]*(2-rgba[3]);
mr.color = 'rgba('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+','+alpha+')';
mr.init();
mr.draw(s.gridData[neighbor.pointIndex][0], s.gridData[neighbor.pointIndex][1], hl.highlightCanvas._ctx);
}
function showTooltip(plot, series, neighbor) {
// neighbor looks like: {seriesIndex: i, pointIndex:j, gridData:p, data:s.data[j]}
// gridData should be x,y pixel coords on the grid.
// add the plot._gridPadding to that to get x,y in the target.
var hl = plot.plugins.highlighter;
var elem = hl._tooltipElem;
var serieshl = series.highlighter || {};
var opts = $.extend(true, {}, hl, serieshl);
if (opts.useAxesFormatters) {
var xf = series._xaxis._ticks[0].formatter;
var yf = series._yaxis._ticks[0].formatter;
var xfstr = series._xaxis._ticks[0].formatString;
var yfstr = series._yaxis._ticks[0].formatString;
var str;
var xstr = xf(xfstr, neighbor.data[0]);
var ystrs = [];
for (var i=1; i<opts.yvalues+1; i++) {
ystrs.push(yf(yfstr, neighbor.data[i]));
}
if (typeof opts.formatString === 'string') {
switch (opts.tooltipAxes) {
case 'both':
case 'xy':
ystrs.unshift(xstr);
ystrs.unshift(opts.formatString);
str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
break;
case 'yx':
ystrs.push(xstr);
ystrs.unshift(opts.formatString);
str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
break;
case 'x':
str = $.jqplot.sprintf.apply($.jqplot.sprintf, [opts.formatString, xstr]);
break;
case 'y':
ystrs.unshift(opts.formatString);
str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
break;
default: // same as xy
ystrs.unshift(xstr);
ystrs.unshift(opts.formatString);
str = $.jqplot.sprintf.apply($.jqplot.sprintf, ystrs);
break;
}
}
else {
switch (opts.tooltipAxes) {
case 'both':
case 'xy':
str = xstr;
for (var i=0; i<ystrs.length; i++) {
str += opts.tooltipSeparator + ystrs[i];
}
break;
case 'yx':
str = '';
for (var i=0; i<ystrs.length; i++) {
str += ystrs[i] + opts.tooltipSeparator;
}
str += xstr;
break;
case 'x':
str = xstr;
break;
case 'y':
str = ystrs.join(opts.tooltipSeparator);
break;
default: // same as 'xy'
str = xstr;
for (var i=0; i<ystrs.length; i++) {
str += opts.tooltipSeparator + ystrs[i];
}
break;
}
}
}
else {
var str;
if (typeof opts.formatString === 'string') {
str = $.jqplot.sprintf.apply($.jqplot.sprintf, [opts.formatString].concat(neighbor.data));
}
else {
if (opts.tooltipAxes == 'both' || opts.tooltipAxes == 'xy') {
str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[0]) + opts.tooltipSeparator + $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[1]);
}
else if (opts.tooltipAxes == 'yx') {
str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[1]) + opts.tooltipSeparator + $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[0]);
}
else if (opts.tooltipAxes == 'x') {
str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[0]);
}
else if (opts.tooltipAxes == 'y') {
str = $.jqplot.sprintf(opts.tooltipFormatString, neighbor.data[1]);
}
}
}
if ($.isFunction(opts.tooltipContentEditor)) {
// args str, seriesIndex, pointIndex are essential so the hook can look up
// extra data for the point.
str = opts.tooltipContentEditor(str, neighbor.seriesIndex, neighbor.pointIndex, plot);
}
elem.html(str);
var gridpos = {x:neighbor.gridData[0], y:neighbor.gridData[1]};
var ms = 0;
var fact = 0.707;
if (series.markerRenderer.show == true) {
ms = (series.markerRenderer.size + opts.sizeAdjust)/2;
}
var loc = locations;
if (series.fillToZero && series.fill && neighbor.data[1] < 0) {
loc = oppositeLocations;
}
switch (loc[locationIndicies[opts.tooltipLocation]]) {
case 'nw':
var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - fact * ms;
var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - fact * ms;
break;
case 'n':
var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true)/2;
var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - ms;
break;
case 'ne':
var x = gridpos.x + plot._gridPadding.left + opts.tooltipOffset + fact * ms;
var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - fact * ms;
break;
case 'e':
var x = gridpos.x + plot._gridPadding.left + opts.tooltipOffset + ms;
var y = gridpos.y + plot._gridPadding.top - elem.outerHeight(true)/2;
break;
case 'se':
var x = gridpos.x + plot._gridPadding.left + opts.tooltipOffset + fact * ms;
var y = gridpos.y + plot._gridPadding.top + opts.tooltipOffset + fact * ms;
break;
case 's':
var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true)/2;
var y = gridpos.y + plot._gridPadding.top + opts.tooltipOffset + ms;
break;
case 'sw':
var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - fact * ms;
var y = gridpos.y + plot._gridPadding.top + opts.tooltipOffset + fact * ms;
break;
case 'w':
var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - ms;
var y = gridpos.y + plot._gridPadding.top - elem.outerHeight(true)/2;
break;
default: // same as 'nw'
var x = gridpos.x + plot._gridPadding.left - elem.outerWidth(true) - opts.tooltipOffset - fact * ms;
var y = gridpos.y + plot._gridPadding.top - opts.tooltipOffset - elem.outerHeight(true) - fact * ms;
break;
}
elem.css('left', x);
elem.css('top', y);
if (opts.fadeTooltip) {
// Fix for stacked up animations. Thnanks Trevor!
elem.stop(true,true).fadeIn(opts.tooltipFadeSpeed);
}
else {
elem.show();
}
elem = null;
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
var hl = plot.plugins.highlighter;
var c = plot.plugins.cursor;
if (hl.show) {
if (neighbor == null && hl.isHighlighting) {
var evt = jQuery.Event('jqplotHighlighterUnhighlight');
plot.target.trigger(evt);
var ctx = hl.highlightCanvas._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
if (hl.fadeTooltip) {
hl._tooltipElem.fadeOut(hl.tooltipFadeSpeed);
}
else {
hl._tooltipElem.hide();
}
if (hl.bringSeriesToFront) {
plot.restorePreviousSeriesOrder();
}
hl.isHighlighting = false;
hl.currentNeighbor = null;
ctx = null;
}
else if (neighbor != null && plot.series[neighbor.seriesIndex].showHighlight && !hl.isHighlighting) {
var evt = jQuery.Event('jqplotHighlighterHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data, plot];
plot.target.trigger(evt, ins);
hl.isHighlighting = true;
hl.currentNeighbor = neighbor;
if (hl.showMarker) {
draw(plot, neighbor);
}
if (plot.series[neighbor.seriesIndex].show && hl.showTooltip && (!c || !c._zoom.started)) {
showTooltip(plot, plot.series[neighbor.seriesIndex], neighbor);
}
if (hl.bringSeriesToFront) {
plot.moveSeriesToFront(neighbor.seriesIndex);
}
}
// check to see if we're highlighting the wrong point.
else if (neighbor != null && hl.isHighlighting && hl.currentNeighbor != neighbor) {
// highlighting the wrong point.
// if new series allows highlighting, highlight new point.
if (plot.series[neighbor.seriesIndex].showHighlight) {
var ctx = hl.highlightCanvas._ctx;
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
hl.isHighlighting = true;
hl.currentNeighbor = neighbor;
if (hl.showMarker) {
draw(plot, neighbor);
}
if (plot.series[neighbor.seriesIndex].show && hl.showTooltip && (!c || !c._zoom.started)) {
showTooltip(plot, plot.series[neighbor.seriesIndex], neighbor);
}
if (hl.bringSeriesToFront) {
plot.moveSeriesToFront(neighbor.seriesIndex);
}
}
}
}
}
})(jQuery);

View File

@@ -0,0 +1,475 @@
/*
2010-11-01 Chris Leonello
Slightly modified version of the original json2.js to put JSON
functions under the $.jqplot namespace.
licensing and orignal comments follow:
http://www.JSON.org/json2.js
2010-08-25
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
$.jqplot.JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
$.jqplot.JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = $.jqplot.JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = $.jqplot.JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = $.jqplot.JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
$.jqplot.JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = $.jqplot.JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = $.jqplot.JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
(function($) {
$.jqplot.JSON = window.JSON;
if (!window.JSON) {
$.jqplot.JSON = {};
}
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf()) ?
this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z' : null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ?
'"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string' ? c :
'\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' :
'"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0 ? '[]' :
gap ? '[\n' + gap +
partial.join(',\n' + gap) + '\n' +
mind + ']' :
'[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
k = rep[i];
if (typeof k === 'string') {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0 ? '{}' :
gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
mind + '}' : '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof $.jqplot.JSON.stringify !== 'function') {
$.jqplot.JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('$.jqplot.JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof $.jqplot.JSON.parse !== 'function') {
$.jqplot.JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function' ?
walk({'': j}, '') : j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('$.jqplot.JSON.parse');
};
}
})(jQuery);

View File

@@ -0,0 +1,534 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* class: $.jqplot.LogAxisRenderer
* A plugin for a jqPlot to render a logarithmic axis.
*
* To use this renderer, include the plugin in your source
* > <script type="text/javascript" language="javascript" src="plugins/jqplot.logAxisRenderer.js"></script>
*
* and supply the appropriate options to your plot
*
* > {axes:{xaxis:{renderer:$.jqplot.LogAxisRenderer}}}
**/
$.jqplot.LogAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
// prop: axisDefaults
// Default properties which will be applied directly to the series.
//
// Group: Properties
//
// Properties
//
// base - the logarithmic base, commonly 2, 10 or Math.E
// tickDistribution - Deprecated. "power" distribution of ticks
// always used. Option has no effect.
this.axisDefaults = {
base : 10,
tickDistribution :'power'
};
};
$.jqplot.LogAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.LogAxisRenderer.prototype.constructor = $.jqplot.LogAxisRenderer;
$.jqplot.LogAxisRenderer.prototype.init = function(options) {
// prop: drawBaseline
// True to draw the axis baseline.
this.drawBaseline = true;
// prop: minorTicks
// Number of ticks to add between "major" ticks.
// Major ticks are ticks supplied by user or auto computed.
// Minor ticks cannot be created by user.
this.minorTicks = 'auto';
this._scalefact = 1.0;
$.extend(true, this, options);
this._autoFormatString = '%d';
this._overrideFormatString = false;
for (var d in this.renderer.axisDefaults) {
if (this[d] == null) {
this[d] = this.renderer.axisDefaults[d];
}
}
this.resetDataBounds();
};
$.jqplot.LogAxisRenderer.prototype.createTicks = function(plot) {
// we're are operating on an axis here
var ticks = this._ticks;
var userTicks = this.ticks;
var name = this.name;
var db = this._dataBounds;
var dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
var interval;
var min, max;
var pos1, pos2;
var tt, i;
var threshold = 30;
// For some reason scalefactor is screwing up ticks.
this._scalefact = (Math.max(dim, threshold+1) - threshold)/300;
// if we already have ticks, use them.
// ticks must be in order of increasing value.
if (userTicks.length) {
// ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
for (i=0; i<userTicks.length; i++){
var ut = userTicks[i];
var t = new this.tickRenderer(this.tickOptions);
if (ut.constructor == Array) {
t.value = ut[0];
t.label = ut[1];
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(ut[0], this.name);
this._ticks.push(t);
}
else if ($.isPlainObject(ut)) {
$.extend(true, t, ut);
t.axis = this.name;
this._ticks.push(t);
}
else {
t.value = ut;
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(ut, this.name);
this._ticks.push(t);
}
}
this.numberTicks = userTicks.length;
this.min = this._ticks[0].value;
this.max = this._ticks[this.numberTicks-1].value;
}
// we don't have any ticks yet, let's make some!
else if (this.min == null && this.max == null) {
min = db.min * (2 - this.padMin);
max = db.max * this.padMax;
// if min and max are same, space them out a bit
if (min == max) {
var adj = 0.05;
min = min*(1-adj);
max = max*(1+adj);
}
// perform some checks
if (this.min != null && this.min <= 0) {
throw new Error("Log axis minimum must be greater than 0");
}
if (this.max != null && this.max <= 0) {
throw new Error("Log axis maximum must be greater than 0");
}
function findCeil (val) {
var order = Math.pow(10, Math.floor(Math.log(val)/Math.LN10));
return Math.ceil(val/order) * order;
}
function findFloor(val) {
var order = Math.pow(10, Math.floor(Math.log(val)/Math.LN10));
return Math.floor(val/order) * order;
}
// var range = max - min;
var rmin, rmax;
// for power distribution, open up range to get a nice power of axis.renderer.base.
// power distribution won't respect the user's min/max settings.
rmin = Math.pow(this.base, Math.floor(Math.log(min)/Math.log(this.base)));
rmax = Math.pow(this.base, Math.ceil(Math.log(max)/Math.log(this.base)));
// // if min and max are same, space them out a bit
// if (rmin === rmax) {
// var adj = 0.05;
// rmin = rmin*(1-adj);
// rmax = rmax*(1+adj);
// }
// Handle case where a data value was zero
if (rmin === 0) {
rmin = 1;
}
var order = Math.round(Math.log(rmin)/Math.LN10);
if (this.tickOptions == null || !this.tickOptions.formatString) {
this._overrideFormatString = true;
}
this.min = rmin;
this.max = rmax;
var range = this.max - this.min;
var minorTicks = (this.minorTicks === 'auto') ? 0 : this.minorTicks;
var numberTicks;
if (this.numberTicks == null){
if (dim > 140) {
numberTicks = Math.round(Math.log(this.max/this.min)/Math.log(this.base) + 1);
if (numberTicks < 2) {
numberTicks = 2;
}
if (minorTicks === 0) {
var temp = dim/(numberTicks - 1);
if (temp < 100) {
minorTicks = 0;
}
else if (temp < 190) {
minorTicks = 1;
}
else if (temp < 250) {
minorTicks = 3;
}
else if (temp < 600) {
minorTicks = 4;
}
else {
minorTicks = 9;
}
}
}
else {
numberTicks = 2;
if (minorTicks === 0) {
minorTicks = 1;
}
minorTicks = 0;
}
}
else {
numberTicks = this.numberTicks;
}
if (order >= 0 && minorTicks !== 3) {
this._autoFormatString = '%d';
}
// Adjust format string for case with 3 ticks where we'll have like 1, 2.5, 5, 7.5, 10
else if (order <= 0 && minorTicks === 3) {
var temp = -(order - 1);
this._autoFormatString = '%.'+ Math.abs(order-1) + 'f';
}
// Adjust format string for values less than 1.
else if (order < 0) {
var temp = -order;
this._autoFormatString = '%.'+ Math.abs(order) + 'f';
}
else {
this._autoFormatString = '%d';
}
var to, t, val, tt1, spread, interval;
for (var i=0; i<numberTicks; i++){
tt = Math.pow(this.base, i - numberTicks + 1) * this.max;
t = new this.tickRenderer(this.tickOptions);
if (this._overrideFormatString) {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(tt, this.name);
this._ticks.push(t);
if (minorTicks && i<numberTicks-1) {
tt1 = Math.pow(this.base, i - numberTicks + 2) * this.max;
spread = tt1 - tt;
interval = tt1 / (minorTicks+1);
for (var j=minorTicks-1; j>=0; j--) {
val = tt1-interval*(j+1);
t = new this.tickRenderer(this.tickOptions);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(val, this.name);
this._ticks.push(t);
}
}
}
}
// min and max are set as would be the case with zooming
else if (this.min != null && this.max != null) {
var opts = $.extend(true, {}, this.tickOptions, {name: this.name, value: null});
var nt, ti;
// don't have an interval yet, pick one that gives the most
// "round" ticks we can get.
if (this.numberTicks == null && this.tickInterval == null) {
// var threshold = 30;
var tdim = Math.max(dim, threshold+1);
var nttarget = Math.ceil((tdim-threshold)/35 + 1);
var ret = $.jqplot.LinearTickGenerator.bestConstrainedInterval(this.min, this.max, nttarget);
this._autoFormatString = ret[3];
nt = ret[2];
ti = ret[4];
for (var i=0; i<nt; i++) {
opts.value = this.min + i * ti;
t = new this.tickRenderer(opts);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
this._ticks.push(t);
}
}
// for loose zoom, number ticks and interval are also set.
else if (this.numberTicks != null && this.tickInterval != null) {
nt = this.numberTicks;
for (var i=0; i<nt; i++) {
opts.value = this.min + i * this.tickInterval;
t = new this.tickRenderer(opts);
if (this._overrideFormatString && this._autoFormatString != '') {
t.formatString = this._autoFormatString;
}
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
this._ticks.push(t);
}
}
}
};
$.jqplot.LogAxisRenderer.prototype.pack = function(pos, offsets) {
var lb = parseInt(this.base, 10);
var ticks = this._ticks;
var trans = function (v) { return Math.log(v)/Math.log(lb); };
var invtrans = function (v) { return Math.pow(Math.E, (Math.log(lb)*v)); };
var max = trans(this.max);
var min = trans(this.min);
var offmax = offsets.max;
var offmin = offsets.min;
var lshow = (this._label == null) ? false : this._label.show;
for (var p in pos) {
this._elem.css(p, pos[p]);
}
this._offsets = offsets;
// pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
var pixellength = offmax - offmin;
var unitlength = max - min;
// point to unit and unit to point conversions references to Plot DOM element top left corner.
this.p2u = function(p){
return invtrans((p - offmin) * unitlength / pixellength + min);
};
this.u2p = function(u){
return (trans(u) - min) * pixellength / unitlength + offmin;
};
if (this.name == 'xaxis' || this.name == 'x2axis'){
this.series_u2p = function(u){
return (trans(u) - min) * pixellength / unitlength;
};
this.series_p2u = function(p){
return invtrans(p * unitlength / pixellength + min);
};
}
// yaxis is max at top of canvas.
else {
this.series_u2p = function(u){
return (trans(u) - max) * pixellength / unitlength;
};
this.series_p2u = function(p){
return invtrans(p * unitlength / pixellength + max);
};
}
if (this.show) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
for (var i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
switch (t.labelPosition) {
case 'auto':
// position at end
if (t.angle < 0) {
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
}
// position at start
else {
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
}
break;
case 'end':
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
case 'start':
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
break;
case 'middle':
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
default:
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
}
}
else {
shim = -t.getWidth()/2;
}
// var shim = t.getWidth()/2;
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('left', val);
t.pack();
}
}
if (lshow) {
var w = this._label._elem.outerWidth(true);
this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
if (this.name == 'xaxis') {
this._label._elem.css('bottom', '0px');
}
else {
this._label._elem.css('top', '0px');
}
this._label.pack();
}
}
else {
for (var i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
switch (t.labelPosition) {
case 'auto':
// position at end
case 'end':
if (t.angle < 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'start':
if (t.angle > 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'middle':
// if (t.angle > 0) {
// shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
// }
// else {
// shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
// }
shim = -t.getHeight()/2;
break;
default:
shim = -t.getHeight()/2;
break;
}
}
else {
shim = -t.getHeight()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('top', val);
t.pack();
}
}
if (lshow) {
var h = this._label._elem.outerHeight(true);
this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
if (this.name == 'yaxis') {
this._label._elem.css('left', '0px');
}
else {
this._label._elem.css('right', '0px');
}
this._label.pack();
}
}
}
};
})(jQuery);

View File

@@ -0,0 +1,611 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// class: $.jqplot.MekkoAxisRenderer
// An axis renderer for a Mekko chart.
// Should be used with a Mekko chart where the mekkoRenderer is used on the series.
// Displays the Y axis as a range from 0 to 1 (0 to 100%) and the x axis with a tick
// for each series scaled to the sum of all the y values.
$.jqplot.MekkoAxisRenderer = function() {
};
// called with scope of axis object.
$.jqplot.MekkoAxisRenderer.prototype.init = function(options){
// prop: tickMode
// How to space the ticks on the axis.
// 'bar' will place a tick at the width of each bar.
// This is the default for the x axis.
// 'even' will place ticks at even intervals. This is
// the default for x2 axis and y axis. y axis cannot be changed.
this.tickMode;
// prop: barLabelRenderer
// renderer to use to draw labels under each bar.
this.barLabelRenderer = $.jqplot.AxisLabelRenderer;
// prop: barLabels
// array of labels to put under each bar.
this.barLabels = this.barLabels || [];
// prop: barLabelOptions
// options object to pass to the bar label renderer.
this.barLabelOptions = {};
this.tickOptions = $.extend(true, {showGridline:false}, this.tickOptions);
this._barLabels = [];
$.extend(true, this, options);
if (this.name == 'yaxis') {
this.tickOptions.formatString = this.tickOptions.formatString || "%d\%";
}
var db = this._dataBounds;
db.min = 0;
// for y axes, scale always go from 0 to 1 (0 to 100%)
if (this.name == 'yaxis' || this.name == 'y2axis') {
db.max = 100;
this.tickMode = 'even';
}
// For x axes, scale goes from 0 to sum of all y values.
else if (this.name == 'xaxis'){
this.tickMode = (this.tickMode == null) ? 'bar' : this.tickMode;
for (var i=0; i<this._series.length; i++) {
db.max += this._series[i]._sumy;
}
}
else if (this.name == 'x2axis'){
this.tickMode = (this.tickMode == null) ? 'even' : this.tickMode;
for (var i=0; i<this._series.length; i++) {
db.max += this._series[i]._sumy;
}
}
};
// called with scope of axis
$.jqplot.MekkoAxisRenderer.prototype.draw = function(ctx, plot) {
if (this.show) {
// populate the axis label and value properties.
// createTicks is a method on the renderer, but
// call it within the scope of the axis.
this.renderer.createTicks.call(this);
// fill a div with axes labels in the right direction.
// Need to pregenerate each axis to get its bounds and
// position it and the labels correctly on the plot.
var dim=0;
var temp;
var elem = document.createElement('div');
this._elem = $(elem);
this._elem.addClass('jqplot-axis jqplot-'+this.name);
this._elem.css('position', 'absolute');
elem = null;
if (this.name == 'xaxis' || this.name == 'x2axis') {
this._elem.width(this._plotDimensions.width);
}
else {
this._elem.height(this._plotDimensions.height);
}
// draw the axis label
// create a _label object.
this.labelOptions.axis = this.name;
this._label = new this.labelRenderer(this.labelOptions);
if (this._label.show) {
this._elem.append(this._label.draw(ctx));
}
var t, tick, elem;
if (this.showTicks) {
t = this._ticks;
for (var i=0; i<t.length; i++) {
tick = t[i];
if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
this._elem.append(tick.draw(ctx));
}
}
}
// draw the series labels
for (i=0; i<this.barLabels.length; i++) {
this.barLabelOptions.axis = this.name;
this.barLabelOptions.label = this.barLabels[i];
this._barLabels.push(new this.barLabelRenderer(this.barLabelOptions));
if (this.tickMode != 'bar') {
this._barLabels[i].show = false;
}
if (this._barLabels[i].show) {
var elem = this._barLabels[i].draw(ctx, plot);
elem.removeClass('jqplot-'+this.name+'-label');
elem.addClass('jqplot-'+this.name+'-tick');
elem.addClass('jqplot-mekko-barLabel');
elem.appendTo(this._elem);
elem = null;
}
}
}
return this._elem;
};
// called with scope of an axis
$.jqplot.MekkoAxisRenderer.prototype.reset = function() {
this.min = this._min;
this.max = this._max;
this.tickInterval = this._tickInterval;
this.numberTicks = this._numberTicks;
// this._ticks = this.__ticks;
};
// called with scope of axis
$.jqplot.MekkoAxisRenderer.prototype.set = function() {
var dim = 0;
var temp;
var w = 0;
var h = 0;
var lshow = (this._label == null) ? false : this._label.show;
if (this.show && this.showTicks) {
var t = this._ticks;
for (var i=0; i<t.length; i++) {
var tick = t[i];
if (tick.showLabel && (!tick.isMinorTick || this.showMinorTicks)) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
temp = tick._elem.outerHeight(true);
}
else {
temp = tick._elem.outerWidth(true);
}
if (temp > dim) {
dim = temp;
}
}
}
if (lshow) {
w = this._label._elem.outerWidth(true);
h = this._label._elem.outerHeight(true);
}
if (this.name == 'xaxis') {
dim = dim + h;
this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
}
else if (this.name == 'x2axis') {
dim = dim + h;
this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
}
else if (this.name == 'yaxis') {
dim = dim + w;
this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css('width', w+'px');
}
}
else {
dim = dim + w;
this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css('width', w+'px');
}
}
}
};
// called with scope of axis
$.jqplot.MekkoAxisRenderer.prototype.createTicks = function() {
// we're are operating on an axis here
var ticks = this._ticks;
var userTicks = this.ticks;
var name = this.name;
// databounds were set on axis initialization.
var db = this._dataBounds;
var dim, interval;
var min, max;
var pos1, pos2;
var t, tt, i, j;
// if we already have ticks, use them.
// ticks must be in order of increasing value.
if (userTicks.length) {
// ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
for (i=0; i<userTicks.length; i++){
var ut = userTicks[i];
var t = new this.tickRenderer(this.tickOptions);
if (ut.constructor == Array) {
t.value = ut[0];
t.label = ut[1];
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(ut[0], this.name);
this._ticks.push(t);
}
else {
t.value = ut;
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(ut, this.name);
this._ticks.push(t);
}
}
this.numberTicks = userTicks.length;
this.min = this._ticks[0].value;
this.max = this._ticks[this.numberTicks-1].value;
this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
}
// we don't have any ticks yet, let's make some!
else {
if (name == 'xaxis' || name == 'x2axis') {
dim = this._plotDimensions.width;
}
else {
dim = this._plotDimensions.height;
}
// if min, max and number of ticks specified, user can't specify interval.
if (this.min != null && this.max != null && this.numberTicks != null) {
this.tickInterval = null;
}
min = (this.min != null) ? this.min : db.min;
max = (this.max != null) ? this.max : db.max;
// if min and max are same, space them out a bit.+
if (min == max) {
var adj = 0.05;
if (min > 0) {
adj = Math.max(Math.log(min)/Math.LN10, 0.05);
}
min -= adj;
max += adj;
}
var range = max - min;
var rmin, rmax;
var temp, prev, curr;
var ynumticks = [3,5,6,11,21];
// yaxis divide ticks in nice intervals from 0 to 1.
if (this.name == 'yaxis' || this.name == 'y2axis') {
this.min = 0;
this.max = 100;
// user didn't specify number of ticks.
if (!this.numberTicks){
if (this.tickInterval) {
this.numberTicks = 3 + Math.ceil(range / this.tickInterval);
}
else {
temp = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
for (i=0; i<ynumticks.length; i++) {
curr = temp/ynumticks[i];
if (curr == 1) {
this.numberTicks = ynumticks[i];
break;
}
else if (curr > 1) {
prev = curr;
continue;
}
else if (curr < 1) {
// was prev or is curr closer to one?
if (Math.abs(prev - 1) < Math.abs(curr - 1)) {
this.numberTicks = ynumticks[i-1];
break;
}
else {
this.numberTicks = ynumticks[i];
break;
}
}
else if (i == ynumticks.length -1) {
this.numberTicks = ynumticks[i];
}
}
this.tickInterval = range / (this.numberTicks - 1);
}
}
// user did specify number of ticks.
else {
this.tickInterval = range / (this.numberTicks - 1);
}
for (var i=0; i<this.numberTicks; i++){
tt = this.min + i * this.tickInterval;
t = new this.tickRenderer(this.tickOptions);
// var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(tt, this.name);
this._ticks.push(t);
}
}
// for x axes, have number ot ticks equal to number of series and ticks placed
// at sum of y values for each series.
else if (this.tickMode == 'bar') {
this.min = 0;
this.numberTicks = this._series.length + 1;
t = new this.tickRenderer(this.tickOptions);
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(0, this.name);
this._ticks.push(t);
temp = 0;
for (i=1; i<this.numberTicks; i++){
temp += this._series[i-1]._sumy;
t = new this.tickRenderer(this.tickOptions);
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(temp, this.name);
this._ticks.push(t);
}
this.max = this.max || temp;
// if user specified a max and it is greater than sum, add a tick
if (this.max > temp) {
t = new this.tickRenderer(this.tickOptions);
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(this.max, this.name);
this._ticks.push(t);
}
}
else if (this.tickMode == 'even') {
this.min = 0;
this.max = this.max || db.max;
// get a desired number of ticks
var nt = 2 + Math.ceil((dim-(this.tickSpacing-1))/this.tickSpacing);
range = this.max - this.min;
this.numberTicks = nt;
this.tickInterval = range / (this.numberTicks - 1);
for (i=0; i<this.numberTicks; i++){
tt = this.min + i * this.tickInterval;
t = new this.tickRenderer(this.tickOptions);
// var t = new $.jqplot.AxisTickRenderer(this.tickOptions);
if (!this.showTicks) {
t.showLabel = false;
t.showMark = false;
}
else if (!this.showTickMarks) {
t.showMark = false;
}
t.setTick(tt, this.name);
this._ticks.push(t);
}
}
}
};
// called with scope of axis
$.jqplot.MekkoAxisRenderer.prototype.pack = function(pos, offsets) {
var ticks = this._ticks;
var max = this.max;
var min = this.min;
var offmax = offsets.max;
var offmin = offsets.min;
var lshow = (this._label == null) ? false : this._label.show;
for (var p in pos) {
this._elem.css(p, pos[p]);
}
this._offsets = offsets;
// pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
var pixellength = offmax - offmin;
var unitlength = max - min;
// point to unit and unit to point conversions references to Plot DOM element top left corner.
this.p2u = function(p){
return (p - offmin) * unitlength / pixellength + min;
};
this.u2p = function(u){
return (u - min) * pixellength / unitlength + offmin;
};
if (this.name == 'xaxis' || this.name == 'x2axis'){
this.series_u2p = function(u){
return (u - min) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + min;
};
}
else {
this.series_u2p = function(u){
return (u - max) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + max;
};
}
if (this.show) {
if (this.name == 'xaxis' || this.name == 'x2axis') {
for (var i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
// will need to adjust auto positioning based on which axis this is.
var temp = (this.name == 'xaxis') ? 1 : -1;
switch (t.labelPosition) {
case 'auto':
// position at end
if (temp * t.angle < 0) {
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
}
// position at start
else {
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
}
break;
case 'end':
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
case 'start':
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
break;
case 'middle':
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
default:
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
}
}
else {
shim = -t.getWidth()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('left', val);
t.pack();
}
}
var w;
if (lshow) {
w = this._label._elem.outerWidth(true);
this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
if (this.name == 'xaxis') {
this._label._elem.css('bottom', '0px');
}
else {
this._label._elem.css('top', '0px');
}
this._label.pack();
}
// now show the labels under the bars.
var b, l, r;
for (var i=0; i<this.barLabels.length; i++) {
b = this._barLabels[i];
if (b.show) {
w = b.getWidth();
l = this._ticks[i].getLeft() + this._ticks[i].getWidth();
r = this._ticks[i+1].getLeft();
b._elem.css('left', (r+l-w)/2+'px');
b._elem.css('top', this._ticks[i]._elem.css('top'));
b.pack();
}
}
}
else {
for (var i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
// will need to adjust auto positioning based on which axis this is.
var temp = (this.name == 'yaxis') ? 1 : -1;
switch (t.labelPosition) {
case 'auto':
// position at end
case 'end':
if (temp * t.angle < 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'start':
if (t.angle > 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'middle':
shim = -t.getHeight()/2;
break;
default:
shim = -t.getHeight()/2;
break;
}
}
else {
shim = -t.getHeight()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('top', val);
t.pack();
}
}
if (lshow) {
var h = this._label._elem.outerHeight(true);
this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
if (this.name == 'yaxis') {
this._label._elem.css('left', '0px');
}
else {
this._label._elem.css('right', '0px');
}
this._label.pack();
}
}
}
};
})(jQuery);

View File

@@ -0,0 +1,437 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.MekkoRenderer
* Draws a Mekko style chart which shows 3 dimensional data on a 2 dimensional graph.
* the <$.jqplot.MekkoAxisRenderer> should be used with mekko charts. The mekko renderer
* overrides the default legend renderer with its own $.jqplot.MekkoLegendRenderer
* which allows more flexibility to specify number of rows and columns in the legend.
*
* Data is specified per bar in the chart. You can specify data as an array of y values, or as
* an array of [label, value] pairs. Note that labels are used only on the first series.
* Labels on subsequent series are ignored:
*
* > bar1 = [['shirts', 8],['hats', 14],['shoes', 6],['gloves', 16],['dolls', 12]];
* > bar2 = [15,6,9,13,6];
* > bar3 = [['grumpy',4],['sneezy',2],['happy',7],['sleepy',9],['doc',7]];
*
* If you want to place labels for each bar under the axis, you use the barLabels option on
* the axes. The bar labels can be styled with the ".jqplot-mekko-barLabel" css class.
*
* > barLabels = ['Mickey Mouse', 'Donald Duck', 'Goofy'];
* > axes:{xaxis:{barLabels:barLabels}}
*
*/
$.jqplot.MekkoRenderer = function(){
this.shapeRenderer = new $.jqplot.ShapeRenderer();
// prop: borderColor
// color of the borders between areas on the chart
this.borderColor = null;
// prop: showBorders
// True to draw borders lines between areas on the chart.
// False will draw borders lines with the same color as the area.
this.showBorders = true;
};
// called with scope of series.
$.jqplot.MekkoRenderer.prototype.init = function(options, plot) {
this.fill = false;
this.fillRect = true;
this.strokeRect = true;
this.shadow = false;
// width of bar on x axis.
this._xwidth = 0;
this._xstart = 0;
$.extend(true, this.renderer, options);
// set the shape renderer options
var opts = {lineJoin:'miter', lineCap:'butt', isarc:false, fillRect:this.fillRect, strokeRect:this.strokeRect};
this.renderer.shapeRenderer.init(opts);
plot.axes.x2axis._series.push(this);
this._type = 'mekko';
};
// Method: setGridData
// converts the user data values to grid coordinates and stores them
// in the gridData array. Will convert user data into appropriate
// rectangles.
// Called with scope of a series.
$.jqplot.MekkoRenderer.prototype.setGridData = function(plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var data = this._plotData;
this.gridData = [];
// figure out width on x axis.
// this._xwidth = this._sumy / plot._sumy * this.canvas.getWidth();
this._xwidth = xp(this._sumy) - xp(0);
if (this.index>0) {
this._xstart = plot.series[this.index-1]._xstart + plot.series[this.index-1]._xwidth;
}
var totheight = this.canvas.getHeight();
var sumy = 0;
var cury;
var curheight;
for (var i=0; i<data.length; i++) {
if (data[i] != null) {
sumy += data[i][1];
cury = totheight - (sumy / this._sumy * totheight);
curheight = data[i][1] / this._sumy * totheight;
this.gridData.push([this._xstart, cury, this._xwidth, curheight]);
}
}
};
// Method: makeGridData
// converts any arbitrary data values to grid coordinates and
// returns them. This method exists so that plugins can use a series'
// linerenderer to generate grid data points without overwriting the
// grid data associated with that series.
// Called with scope of a series.
$.jqplot.MekkoRenderer.prototype.makeGridData = function(data, plot) {
// recalculate the grid data
// figure out width on x axis.
var xp = this._xaxis.series_u2p;
var totheight = this.canvas.getHeight();
var sumy = 0;
var cury;
var curheight;
var gd = [];
for (var i=0; i<data.length; i++) {
if (data[i] != null) {
sumy += data[i][1];
cury = totheight - (sumy / this._sumy * totheight);
curheight = data[i][1] / this._sumy * totheight;
gd.push([this._xstart, cury, this._xwidth, curheight]);
}
}
return gd;
};
// called within scope of series.
$.jqplot.MekkoRenderer.prototype.draw = function(ctx, gd, options) {
var i;
var opts = (options != undefined) ? options : {};
var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
ctx.save();
if (gd.length) {
if (showLine) {
for (i=0; i<gd.length; i++){
opts.fillStyle = colorGenerator.next();
if (this.renderer.showBorders) {
opts.strokeStyle = this.renderer.borderColor;
}
else {
opts.strokeStyle = opts.fillStyle;
}
this.renderer.shapeRenderer.draw(ctx, gd[i], opts);
}
}
}
ctx.restore();
};
$.jqplot.MekkoRenderer.prototype.drawShadow = function(ctx, gd, options) {
// This is a no-op, no shadows on mekko charts.
};
/**
* Class: $.jqplot.MekkoLegendRenderer
* Legend renderer used by mekko charts with options for
* controlling number or rows and columns as well as placement
* outside of plot area.
*
*/
$.jqplot.MekkoLegendRenderer = function(){
//
};
$.jqplot.MekkoLegendRenderer.prototype.init = function(options) {
// prop: numberRows
// Maximum number of rows in the legend. 0 or null for unlimited.
this.numberRows = null;
// prop: numberColumns
// Maximum number of columns in the legend. 0 or null for unlimited.
this.numberColumns = null;
// this will override the placement option on the Legend object
this.placement = "outside";
$.extend(true, this, options);
};
// called with scope of legend
$.jqplot.MekkoLegendRenderer.prototype.draw = function() {
var legend = this;
if (this.show) {
var series = this._series;
var ss = 'position:absolute;';
ss += (this.background) ? 'background:'+this.background+';' : '';
ss += (this.border) ? 'border:'+this.border+';' : '';
ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
// Mekko charts legends don't go by number of series, but by number of data points
// in the series. Refactor things here for that.
var pad = false,
reverse = true, // mekko charts are always stacked, so reverse
nr, nc;
var s = series[0];
var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
if (s.show) {
var pd = s.data;
if (this.numberRows) {
nr = this.numberRows;
if (!this.numberColumns){
nc = Math.ceil(pd.length/nr);
}
else{
nc = this.numberColumns;
}
}
else if (this.numberColumns) {
nc = this.numberColumns;
nr = Math.ceil(pd.length/this.numberColumns);
}
else {
nr = pd.length;
nc = 1;
}
var i, j, tr, td1, td2, lt, rs, color;
var idx = 0;
for (i=0; i<nr; i++) {
if (reverse){
tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
}
else{
tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
}
for (j=0; j<nc; j++) {
if (idx < pd.length) {
lt = this.labels[idx] || pd[idx][0].toString();
color = colorGenerator.next();
if (!reverse){
if (i>0){
pad = true;
}
else{
pad = false;
}
}
else{
if (i == nr -1){
pad = false;
}
else{
pad = true;
}
}
rs = (pad) ? this.rowSpacing : '0';
td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
'<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
'</div></td>');
td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
if (this.escapeHtml){
td2.text(lt);
}
else {
td2.html(lt);
}
if (reverse) {
td2.prependTo(tr);
td1.prependTo(tr);
}
else {
td1.appendTo(tr);
td2.appendTo(tr);
}
pad = true;
}
idx++;
}
}
tr = null;
td1 = null;
td2 = null;
}
}
return this._elem;
};
$.jqplot.MekkoLegendRenderer.prototype.pack = function(offsets) {
if (this.show) {
// fake a grid for positioning
var grid = {_top:offsets.top, _left:offsets.left, _right:offsets.right, _bottom:this._plotDimensions.height - offsets.bottom};
if (this.placement == 'insideGrid') {
switch (this.location) {
case 'nw':
var a = grid._left + this.xoffset;
var b = grid._top + this.yoffset;
this._elem.css('left', a);
this._elem.css('top', b);
break;
case 'n':
var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
var b = grid._top + this.yoffset;
this._elem.css('left', a);
this._elem.css('top', b);
break;
case 'ne':
var a = offsets.right + this.xoffset;
var b = grid._top + this.yoffset;
this._elem.css({right:a, top:b});
break;
case 'e':
var a = offsets.right + this.xoffset;
var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
this._elem.css({right:a, top:b});
break;
case 'se':
var a = offsets.right + this.xoffset;
var b = offsets.bottom + this.yoffset;
this._elem.css({right:a, bottom:b});
break;
case 's':
var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
var b = offsets.bottom + this.yoffset;
this._elem.css({left:a, bottom:b});
break;
case 'sw':
var a = grid._left + this.xoffset;
var b = offsets.bottom + this.yoffset;
this._elem.css({left:a, bottom:b});
break;
case 'w':
var a = grid._left + this.xoffset;
var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
this._elem.css({left:a, top:b});
break;
default: // same as 'se'
var a = grid._right - this.xoffset;
var b = grid._bottom + this.yoffset;
this._elem.css({right:a, bottom:b});
break;
}
}
else {
switch (this.location) {
case 'nw':
var a = this._plotDimensions.width - grid._left + this.xoffset;
var b = grid._top + this.yoffset;
this._elem.css('right', a);
this._elem.css('top', b);
break;
case 'n':
var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
var b = this._plotDimensions.height - grid._top + this.yoffset;
this._elem.css('left', a);
this._elem.css('bottom', b);
break;
case 'ne':
var a = this._plotDimensions.width - offsets.right + this.xoffset;
var b = grid._top + this.yoffset;
this._elem.css({left:a, top:b});
break;
case 'e':
var a = this._plotDimensions.width - offsets.right + this.xoffset;
var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
this._elem.css({left:a, top:b});
break;
case 'se':
var a = this._plotDimensions.width - offsets.right + this.xoffset;
var b = offsets.bottom + this.yoffset;
this._elem.css({left:a, bottom:b});
break;
case 's':
var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
var b = this._plotDimensions.height - offsets.bottom + this.yoffset;
this._elem.css({left:a, top:b});
break;
case 'sw':
var a = this._plotDimensions.width - grid._left + this.xoffset;
var b = offsets.bottom + this.yoffset;
this._elem.css({right:a, bottom:b});
break;
case 'w':
var a = this._plotDimensions.width - grid._left + this.xoffset;
var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
this._elem.css({right:a, top:b});
break;
default: // same as 'se'
var a = grid._right - this.xoffset;
var b = grid._bottom + this.yoffset;
this._elem.css({right:a, bottom:b});
break;
}
}
}
};
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = options.axesDefaults || {};
options.legend = options.legend || {};
options.seriesDefaults = options.seriesDefaults || {};
var setopts = false;
if (options.seriesDefaults.renderer == $.jqplot.MekkoRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer == $.jqplot.MekkoRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.MekkoAxisRenderer;
options.legend.renderer = $.jqplot.MekkoLegendRenderer;
options.legend.preDraw = true;
}
}
$.jqplot.preInitHooks.push(preInit);
})(jQuery);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,45 @@
/**
* jqplot.jquerymobile plugin
* jQuery Mobile virtual event support.
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2011 Takashi Okamoto
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
*/
(function($) {
function postInit(target, data, options){
this.bindCustomEvents = function() {
this.eventCanvas._elem.bind('vclick', {plot:this}, this.onClick);
this.eventCanvas._elem.bind('dblclick', {plot:this}, this.onDblClick);
this.eventCanvas._elem.bind('taphold', {plot:this}, this.onDblClick);
this.eventCanvas._elem.bind('vmousedown', {plot:this}, this.onMouseDown);
this.eventCanvas._elem.bind('vmousemove', {plot:this}, this.onMouseMove);
this.eventCanvas._elem.bind('mouseenter', {plot:this}, this.onMouseEnter);
this.eventCanvas._elem.bind('mouseleave', {plot:this}, this.onMouseLeave);
if (this.captureRightClick) {
this.eventCanvas._elem.bind('vmouseup', {plot:this}, this.onRightClick);
this.eventCanvas._elem.get(0).oncontextmenu = function() {
return false;
};
}
else {
this.eventCanvas._elem.bind('vmouseup', {plot:this}, this.onMouseUp);
}
};
this.plugins.mobile = true;
}
$.jqplot.postInitHooks.push(postInit);
})(jQuery);

View File

@@ -0,0 +1,373 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.OHLCRenderer
* jqPlot Plugin to draw Open Hi Low Close, Candlestick and Hi Low Close charts.
*
* To use this plugin, include the renderer js file in
* your source:
*
* > <script type="text/javascript" src="plugins/jqplot.ohlcRenderer.js"></script>
*
* You will most likely want to use a date axis renderer
* for the x axis also, so include the date axis render js file also:
*
* > <script type="text/javascript" src="plugins/jqplot.dateAxisRenderer.js"></script>
*
* Then you set the renderer in the series options on your plot:
*
* > series: [{renderer:$.jqplot.OHLCRenderer}]
*
* For OHLC and candlestick charts, data should be specified
* like so:
*
* > dat = [['07/06/2009',138.7,139.68,135.18,135.4], ['06/29/2009',143.46,144.66,139.79,140.02], ...]
*
* If the data array has only 4 values per point instead of 5,
* the renderer will create a Hi Low Close chart instead. In that case,
* data should be supplied like:
*
* > dat = [['07/06/2009',139.68,135.18,135.4], ['06/29/2009',144.66,139.79,140.02], ...]
*
* To generate a candlestick chart instead of an OHLC chart,
* set the "candlestick" option to true:
*
* > series: [{renderer:$.jqplot.OHLCRenderer, rendererOptions:{candleStick:true}}],
*
*/
$.jqplot.OHLCRenderer = function(){
// subclass line renderer to make use of some of its methods.
$.jqplot.LineRenderer.call(this);
// prop: candleStick
// true to render chart as candleStick.
// Must have an open price, cannot be a hlc chart.
this.candleStick = false;
// prop: tickLength
// length of the line in pixels indicating open and close price.
// Default will auto calculate based on plot width and
// number of points displayed.
this.tickLength = 'auto';
// prop: bodyWidth
// width of the candlestick body in pixels. Default will auto calculate
// based on plot width and number of candlesticks displayed.
this.bodyWidth = 'auto';
// prop: openColor
// color of the open price tick mark. Default is series color.
this.openColor = null;
// prop: closeColor
// color of the close price tick mark. Default is series color.
this.closeColor = null;
// prop: wickColor
// color of the hi-lo line thorugh the candlestick body.
// Default is the series color.
this.wickColor = null;
// prop: fillUpBody
// true to render an "up" day (close price greater than open price)
// with a filled candlestick body.
this.fillUpBody = false;
// prop: fillDownBody
// true to render a "down" day (close price lower than open price)
// with a filled candlestick body.
this.fillDownBody = true;
// prop: upBodyColor
// Color of candlestick body of an "up" day. Default is series color.
this.upBodyColor = null;
// prop: downBodyColor
// Color of candlestick body on a "down" day. Default is series color.
this.downBodyColor = null;
// prop: hlc
// true if is a hi-low-close chart (no open price).
// This is determined automatically from the series data.
this.hlc = false;
// prop: lineWidth
// Width of the hi-low line and open/close ticks.
// Must be set in the rendererOptions for the series.
this.lineWidth = 1.5;
this._tickLength;
this._bodyWidth;
};
$.jqplot.OHLCRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.OHLCRenderer.prototype.constructor = $.jqplot.OHLCRenderer;
// called with scope of series.
$.jqplot.OHLCRenderer.prototype.init = function(options) {
options = options || {};
// lineWidth has to be set on the series, changes in renderer
// constructor have no effect. set the default here
// if no renderer option for lineWidth is specified.
this.lineWidth = options.lineWidth || 1.5;
$.jqplot.LineRenderer.prototype.init.call(this, options);
this._type = 'ohlc';
// set the yaxis data bounds here to account for hi and low values
var db = this._yaxis._dataBounds;
var d = this._plotData;
// if data points have less than 5 values, force a hlc chart.
if (d[0].length < 5) {
this.renderer.hlc = true;
for (var j=0; j<d.length; j++) {
if (d[j][2] < db.min || db.min == null) {
db.min = d[j][2];
}
if (d[j][1] > db.max || db.max == null) {
db.max = d[j][1];
}
}
}
else {
for (var j=0; j<d.length; j++) {
if (d[j][3] < db.min || db.min == null) {
db.min = d[j][3];
}
if (d[j][2] > db.max || db.max == null) {
db.max = d[j][2];
}
}
}
};
// called within scope of series.
$.jqplot.OHLCRenderer.prototype.draw = function(ctx, gd, options) {
var d = this.data;
var xmin = this._xaxis.min;
var xmax = this._xaxis.max;
// index of last value below range of plot.
var xminidx = 0;
// index of first value above range of plot.
var xmaxidx = d.length;
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var i, prevColor, ops, b, h, w, a, points;
var o;
var r = this.renderer;
var opts = (options != undefined) ? options : {};
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
var fillAndStroke = (opts.fillAndStroke != undefined) ? opts.fillAndStroke : this.fillAndStroke;
r.bodyWidth = (opts.bodyWidth != undefined) ? opts.bodyWidth : r.bodyWidth;
r.tickLength = (opts.tickLength != undefined) ? opts.tickLength : r.tickLength;
ctx.save();
if (this.show) {
var x, open, hi, low, close;
// need to get widths based on number of points shown,
// not on total number of points. Use the results
// to speed up drawing in next step.
for (var i=0; i<d.length; i++) {
if (d[i][0] < xmin) {
xminidx = i;
}
else if (d[i][0] < xmax) {
xmaxidx = i+1;
}
}
var dwidth = this.gridData[xmaxidx-1][0] - this.gridData[xminidx][0];
var nvisiblePoints = xmaxidx - xminidx;
try {
var dinterval = Math.abs(this._xaxis.series_u2p(parseInt(this._xaxis._intervalStats[0].sortedIntervals[0].interval, 10)) - this._xaxis.series_u2p(0));
}
catch (e) {
var dinterval = dwidth / nvisiblePoints;
}
if (r.candleStick) {
if (typeof(r.bodyWidth) == 'number') {
r._bodyWidth = r.bodyWidth;
}
else {
r._bodyWidth = Math.min(20, dinterval/1.65);
}
}
else {
if (typeof(r.tickLength) == 'number') {
r._tickLength = r.tickLength;
}
else {
r._tickLength = Math.min(10, dinterval/3.5);
}
}
for (var i=xminidx; i<xmaxidx; i++) {
x = xp(d[i][0]);
if (r.hlc) {
open = null;
hi = yp(d[i][1]);
low = yp(d[i][2]);
close = yp(d[i][3]);
}
else {
open = yp(d[i][1]);
hi = yp(d[i][2]);
low = yp(d[i][3]);
close = yp(d[i][4]);
}
o = {};
if (r.candleStick && !r.hlc) {
w = r._bodyWidth;
a = x - w/2;
// draw candle
// determine if candle up or down
// up, remember grid coordinates increase downward
if (close < open) {
// draw wick
if (r.wickColor) {
o.color = r.wickColor;
}
else if (r.downBodyColor) {
o.color = r.upBodyColor;
}
ops = $.extend(true, {}, opts, o);
r.shapeRenderer.draw(ctx, [[x, hi], [x, close]], ops);
r.shapeRenderer.draw(ctx, [[x, open], [x, low]], ops);
o = {};
b = close;
h = open - close;
// if color specified, use it
if (r.fillUpBody) {
o.fillRect = true;
}
else {
o.strokeRect = true;
w = w - this.lineWidth;
a = x - w/2;
}
if (r.upBodyColor) {
o.color = r.upBodyColor;
o.fillStyle = r.upBodyColor;
}
points = [a, b, w, h];
}
// down
else if (close > open) {
// draw wick
if (r.wickColor) {
o.color = r.wickColor;
}
else if (r.downBodyColor) {
o.color = r.downBodyColor;
}
ops = $.extend(true, {}, opts, o);
r.shapeRenderer.draw(ctx, [[x, hi], [x, open]], ops);
r.shapeRenderer.draw(ctx, [[x, close], [x, low]], ops);
o = {};
b = open;
h = close - open;
// if color specified, use it
if (r.fillDownBody) {
o.fillRect = true;
}
else {
o.strokeRect = true;
w = w - this.lineWidth;
a = x - w/2;
}
if (r.downBodyColor) {
o.color = r.downBodyColor;
o.fillStyle = r.downBodyColor;
}
points = [a, b, w, h];
}
// even, open = close
else {
// draw wick
if (r.wickColor) {
o.color = r.wickColor;
}
ops = $.extend(true, {}, opts, o);
r.shapeRenderer.draw(ctx, [[x, hi], [x, low]], ops);
o = {};
o.fillRect = false;
o.strokeRect = false;
a = [x - w/2, open];
b = [x + w/2, close];
w = null;
h = null;
points = [a, b];
}
ops = $.extend(true, {}, opts, o);
r.shapeRenderer.draw(ctx, points, ops);
}
else {
prevColor = opts.color;
if (r.openColor) {
opts.color = r.openColor;
}
// draw open tick
if (!r.hlc) {
r.shapeRenderer.draw(ctx, [[x-r._tickLength, open], [x, open]], opts);
}
opts.color = prevColor;
// draw wick
if (r.wickColor) {
opts.color = r.wickColor;
}
r.shapeRenderer.draw(ctx, [[x, hi], [x, low]], opts);
opts.color = prevColor;
// draw close tick
if (r.closeColor) {
opts.color = r.closeColor;
}
r.shapeRenderer.draw(ctx, [[x, close], [x+r._tickLength, close]], opts);
opts.color = prevColor;
}
}
}
ctx.restore();
};
$.jqplot.OHLCRenderer.prototype.drawShadow = function(ctx, gd, options) {
// This is a no-op, shadows drawn with lines.
};
// called with scope of plot.
$.jqplot.OHLCRenderer.checkOptions = function(target, data, options) {
// provide some sensible highlighter options by default
// These aren't good for hlc, only for ohlc or candlestick
if (!options.highlighter) {
options.highlighter = {
showMarker:false,
tooltipAxes: 'y',
yvalues: 4,
formatString:'<table class="jqplot-highlighter"><tr><td>date:</td><td>%s</td></tr><tr><td>open:</td><td>%s</td></tr><tr><td>hi:</td><td>%s</td></tr><tr><td>low:</td><td>%s</td></tr><tr><td>close:</td><td>%s</td></tr></table>'
};
}
};
//$.jqplot.preInitHooks.push($.jqplot.OHLCRenderer.checkOptions);
})(jQuery);

View File

@@ -0,0 +1,946 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.PieRenderer
* Plugin renderer to draw a pie chart.
* x values, if present, will be used as slice labels.
* y values give slice size.
*
* To use this renderer, you need to include the
* pie renderer plugin, for example:
*
* > <script type="text/javascript" src="plugins/jqplot.pieRenderer.js"></script>
*
* Properties described here are passed into the $.jqplot function
* as options on the series renderer. For example:
*
* > plot2 = $.jqplot('chart2', [s1, s2], {
* > seriesDefaults: {
* > renderer:$.jqplot.PieRenderer,
* > rendererOptions:{
* > sliceMargin: 2,
* > startAngle: -90
* > }
* > }
* > });
*
* A pie plot will trigger events on the plot target
* according to user interaction. All events return the event object,
* the series index, the point (slice) index, and the point data for
* the appropriate slice.
*
* 'jqplotDataMouseOver' - triggered when user mouseing over a slice.
* 'jqplotDataHighlight' - triggered the first time user mouses over a slice,
* if highlighting is enabled.
* 'jqplotDataUnhighlight' - triggered when a user moves the mouse out of
* a highlighted slice.
* 'jqplotLegendHighlight' - triggered the first time user mouses over a legend,
* if highlighting is enabled.
* 'jqplotLegendUnhighlight' - triggered when a user moves the mouse out of
* a highlighted legend.
* 'jqplotDataClick' - triggered when the user clicks on a slice.
* 'jqplotDataRightClick' - tiggered when the user right clicks on a slice if
* the "captureRightClick" option is set to true on the plot.
*/
$.jqplot.PieRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.PieRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.PieRenderer.prototype.constructor = $.jqplot.PieRenderer;
// called with scope of a series
$.jqplot.PieRenderer.prototype.init = function(options, plot) {
// Group: Properties
//
// prop: diameter
// Outer diameter of the pie, auto computed by default
this.diameter = null;
// prop: padding
// padding between the pie and plot edges, legend, etc.
this.padding = 20;
// prop: sliceMargin
// angular spacing between pie slices in degrees.
this.sliceMargin = 0;
// prop: fill
// true or false, whether to fil the slices.
this.fill = true;
// prop: shadowOffset
// offset of the shadow from the slice and offset of
// each succesive stroke of the shadow from the last.
this.shadowOffset = 2;
// prop: shadowAlpha
// transparency of the shadow (0 = transparent, 1 = opaque)
this.shadowAlpha = 0.07;
// prop: shadowDepth
// number of strokes to apply to the shadow,
// each stroke offset shadowOffset from the last.
this.shadowDepth = 5;
// prop: highlightMouseOver
// True to highlight slice when moused over.
// This must be false to enable highlightMouseDown to highlight when clicking on a slice.
this.highlightMouseOver = true;
// prop: highlightMouseDown
// True to highlight when a mouse button is pressed over a slice.
// This will be disabled if highlightMouseOver is true.
this.highlightMouseDown = false;
// prop: highlightColors
// an array of colors to use when highlighting a slice.
this.highlightColors = [];
// prop: dataLabels
// Either 'label', 'value', 'percent' or an array of labels to place on the pie slices.
// Defaults to percentage of each pie slice.
this.dataLabels = 'percent';
// prop: showDataLabels
// true to show data labels on slices.
this.showDataLabels = false;
// prop: dataLabelFormatString
// Format string for data labels. If none, '%s' is used for "label" and for arrays, '%d' for value and '%d%%' for percentage.
this.dataLabelFormatString = null;
// prop: dataLabelThreshold
// Threshhold in percentage (0-100) of pie area, below which no label will be displayed.
// This applies to all label types, not just to percentage labels.
this.dataLabelThreshold = 3;
// prop: dataLabelPositionFactor
// A Multiplier (0-1) of the pie radius which controls position of label on slice.
// Increasing will slide label toward edge of pie, decreasing will slide label toward center of pie.
this.dataLabelPositionFactor = 0.52;
// prop: dataLabelNudge
// Number of pixels to slide the label away from (+) or toward (-) the center of the pie.
this.dataLabelNudge = 2;
// prop: dataLabelCenterOn
// True to center the data label at its position.
// False to set the inside facing edge of the label at its position.
this.dataLabelCenterOn = true;
// prop: startAngle
// Angle to start drawing pie in degrees.
// According to orientation of canvas coordinate system:
// 0 = on the positive x axis
// -90 = on the positive y axis.
// 90 = on the negaive y axis.
// 180 or - 180 = on the negative x axis.
this.startAngle = 0;
this.tickRenderer = $.jqplot.PieTickRenderer;
// prop: showSlice
// Array for whether the pie chart slice for a data element should be displayed.
// Containsg true or false for each data element. If not specified, defaults to true.
this.showSlice = [];
// Used as check for conditions where pie shouldn't be drawn.
this._drawData = true;
this._type = 'pie';
// if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
if (options.highlightMouseDown && options.highlightMouseOver == null) {
options.highlightMouseOver = false;
}
$.extend(true, this, options);
if (this.sliceMargin < 0) {
this.sliceMargin = 0;
}
this._diameter = null;
this._radius = null;
// array of [start,end] angles arrays, one for each slice. In radians.
this._sliceAngles = [];
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// set highlight colors if none provided
if (this.highlightColors.length == 0) {
for (var i=0; i<this.seriesColors.length; i++){
var rgba = $.jqplot.getColorComponents(this.seriesColors[i]);
var newrgb = [rgba[0], rgba[1], rgba[2]];
var sum = newrgb[0] + newrgb[1] + newrgb[2];
for (var j=0; j<3; j++) {
// when darkening, lowest color component can be is 60.
newrgb[j] = (sum > 570) ? newrgb[j] * 0.8 : newrgb[j] + 0.3 * (255 - newrgb[j]);
newrgb[j] = parseInt(newrgb[j], 10);
}
this.highlightColors.push('rgb('+newrgb[0]+','+newrgb[1]+','+newrgb[2]+')');
}
}
this.highlightColorGenerator = new $.jqplot.ColorGenerator(this.highlightColors);
plot.postParseOptionsHooks.addOnce(postParseOptions);
plot.postInitHooks.addOnce(postInit);
plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
plot.eventListenerHooks.addOnce('jqplotMouseDown', handleMouseDown);
plot.eventListenerHooks.addOnce('jqplotMouseUp', handleMouseUp);
plot.eventListenerHooks.addOnce('jqplotClick', handleClick);
plot.eventListenerHooks.addOnce('jqplotRightClick', handleRightClick);
plot.postDrawHooks.addOnce(postPlotDraw);
};
$.jqplot.PieRenderer.prototype.setGridData = function(plot) {
// set gridData property. This will hold angle in radians of each data point.
var stack = [];
var td = [];
var sa = this.startAngle/180*Math.PI;
var tot = 0;
// don't know if we have any valid data yet, so set plot to not draw.
this._drawData = false;
for (var i=0; i<this.data.length; i++){
if (this.data[i][1] != 0) {
// we have data, O.K. to draw.
this._drawData = true;
if (this.showSlice[i] === undefined) {
this.showSlice[i] = true;
}
}
stack.push(this.data[i][1]);
td.push([this.data[i][0]]);
if (i>0) {
stack[i] += stack[i-1];
}
tot += this.data[i][1];
}
var fact = Math.PI*2/stack[stack.length - 1];
for (var i=0; i<stack.length; i++) {
td[i][1] = stack[i] * fact;
td[i][2] = this.data[i][1]/tot;
}
this.gridData = td;
};
$.jqplot.PieRenderer.prototype.makeGridData = function(data, plot) {
var stack = [];
var td = [];
var tot = 0;
var sa = this.startAngle/180*Math.PI;
// don't know if we have any valid data yet, so set plot to not draw.
this._drawData = false;
for (var i=0; i<data.length; i++){
if (this.data[i][1] != 0) {
// we have data, O.K. to draw.
this._drawData = true;
}
stack.push(data[i][1]);
td.push([data[i][0]]);
if (i>0) {
stack[i] += stack[i-1];
}
tot += data[i][1];
}
var fact = Math.PI*2/stack[stack.length - 1];
for (var i=0; i<stack.length; i++) {
td[i][1] = stack[i] * fact;
td[i][2] = data[i][1]/tot;
}
return td;
};
function calcRadiusAdjustment(ang) {
return Math.sin((ang - (ang-Math.PI) / 8 / Math.PI )/2.0);
}
function calcRPrime(ang1, ang2, sliceMargin, fill, lineWidth) {
var rprime = 0;
var ang = ang2 - ang1;
var absang = Math.abs(ang);
var sm = sliceMargin;
if (fill == false) {
sm += lineWidth;
}
if (sm > 0 && absang > 0.01 && absang < 6.282) {
rprime = parseFloat(sm) / 2.0 / calcRadiusAdjustment(ang);
}
return rprime;
}
$.jqplot.PieRenderer.prototype.drawSlice = function (ctx, ang1, ang2, color, isShadow) {
if (this._drawData) {
var r = this._radius;
var fill = this.fill;
var lineWidth = this.lineWidth;
var sm = this.sliceMargin;
if (this.fill == false) {
sm += this.lineWidth;
}
ctx.save();
ctx.translate(this._center[0], this._center[1]);
var rprime = calcRPrime(ang1, ang2, this.sliceMargin, this.fill, this.lineWidth);
var transx = rprime * Math.cos((ang1 + ang2) / 2.0);
var transy = rprime * Math.sin((ang1 + ang2) / 2.0);
if ((ang2 - ang1) <= Math.PI) {
r -= rprime;
}
else {
r += rprime;
}
ctx.translate(transx, transy);
if (isShadow) {
for (var i=0, l=this.shadowDepth; i<l; i++) {
ctx.save();
ctx.translate(this.shadowOffset*Math.cos(this.shadowAngle/180*Math.PI), this.shadowOffset*Math.sin(this.shadowAngle/180*Math.PI));
doDraw(r);
}
for (var i=0, l=this.shadowDepth; i<l; i++) {
ctx.restore();
}
}
else {
doDraw(r);
}
ctx.restore();
}
function doDraw (rad) {
// Fix for IE and Chrome that can't seem to draw circles correctly.
// ang2 should always be <= 2 pi since that is the way the data is converted.
// 2Pi = 6.2831853, Pi = 3.1415927
if (ang2 > 6.282 + this.startAngle) {
ang2 = 6.282 + this.startAngle;
if (ang1 > ang2) {
ang1 = 6.281 + this.startAngle;
}
}
// Fix for IE, where it can't seem to handle 0 degree angles. Also avoids
// ugly line on unfilled pies.
if (ang1 >= ang2) {
return;
}
ctx.beginPath();
ctx.fillStyle = color;
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
ctx.arc(0, 0, rad, ang1, ang2, false);
ctx.lineTo(0,0);
ctx.closePath();
if (fill) {
ctx.fill();
}
else {
ctx.stroke();
}
}
};
// called with scope of series
$.jqplot.PieRenderer.prototype.draw = function (ctx, gd, options, plot) {
var i;
var opts = (options != undefined) ? options : {};
// offset and direction of offset due to legend placement
var offx = 0;
var offy = 0;
var trans = 1;
var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
var sliceColor;
if (options.legendInfo && options.legendInfo.placement == 'insideGrid') {
var li = options.legendInfo;
switch (li.location) {
case 'nw':
offx = li.width + li.xoffset;
break;
case 'w':
offx = li.width + li.xoffset;
break;
case 'sw':
offx = li.width + li.xoffset;
break;
case 'ne':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'e':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'se':
offx = li.width + li.xoffset;
trans = -1;
break;
case 'n':
offy = li.height + li.yoffset;
break;
case 's':
offy = li.height + li.yoffset;
trans = -1;
break;
default:
break;
}
}
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
//see http://stackoverflow.com/questions/20221461/hidpi-retina-plot-drawing
var cw = parseInt(ctx.canvas.style.width);
var ch = parseInt(ctx.canvas.style.height);
//
var w = cw - offx - 2 * this.padding;
var h = ch - offy - 2 * this.padding;
var mindim = Math.min(w,h);
var d = mindim;
// Fixes issue #272. Thanks hugwijst!
// reset slice angles array.
this._sliceAngles = [];
var sm = this.sliceMargin;
if (this.fill == false) {
sm += this.lineWidth;
}
var rprime;
var maxrprime = 0;
var ang, ang1, ang2, shadowColor;
var sa = this.startAngle / 180 * Math.PI;
// have to pre-draw shadows, so loop throgh here and calculate some values also.
for (var i=0, l=gd.length; i<l; i++) {
ang1 = (i == 0) ? sa : gd[i-1][1] + sa;
ang2 = gd[i][1] + sa;
this._sliceAngles.push([ang1, ang2]);
rprime = calcRPrime(ang1, ang2, this.sliceMargin, this.fill, this.lineWidth);
if (Math.abs(ang2-ang1) > Math.PI) {
maxrprime = Math.max(rprime, maxrprime);
}
}
if (this.diameter != null && this.diameter > 0) {
this._diameter = this.diameter - 2*maxrprime;
}
else {
this._diameter = d - 2*maxrprime;
}
// Need to check for undersized pie. This can happen if
// plot area too small and legend is too big.
if (this._diameter < 6) {
$.jqplot.log('Diameter of pie too small, not rendering.');
return;
}
var r = this._radius = this._diameter/2;
this._center = [(cw - trans * offx)/2 + trans * offx + maxrprime * Math.cos(sa), (ch - trans*offy)/2 + trans * offy + maxrprime * Math.sin(sa)];
if (this.shadow) {
for (var i=0, l=gd.length; i<l; i++) {
shadowColor = 'rgba(0,0,0,'+this.shadowAlpha+')';
this.renderer.drawSlice.call (this, ctx, this._sliceAngles[i][0], this._sliceAngles[i][1], shadowColor, true);
}
}
for (var i=0; i<gd.length; i++) {
sliceColor = colorGenerator.next();
if (this.showSlice[i]) {
this.renderer.drawSlice.call (this, ctx, this._sliceAngles[i][0], this._sliceAngles[i][1], sliceColor, false);
if (this.showDataLabels && gd[i][2]*100 >= this.dataLabelThreshold) {
var fstr, avgang = (this._sliceAngles[i][0] + this._sliceAngles[i][1])/2, label;
if (this.dataLabels == 'label') {
fstr = this.dataLabelFormatString || '%s';
label = $.jqplot.sprintf(fstr, gd[i][0]);
}
else if (this.dataLabels == 'value') {
fstr = this.dataLabelFormatString || '%d';
label = $.jqplot.sprintf(fstr, this.data[i][1]);
}
else if (this.dataLabels == 'percent') {
fstr = this.dataLabelFormatString || '%d%%';
label = $.jqplot.sprintf(fstr, gd[i][2]*100);
}
else if (this.dataLabels.constructor == Array) {
fstr = this.dataLabelFormatString || '%s';
label = $.jqplot.sprintf(fstr, this.dataLabels[i]);
}
var fact = (this._radius ) * this.dataLabelPositionFactor + this.sliceMargin + this.dataLabelNudge;
var x = this._center[0] + Math.cos(avgang) * fact + this.canvas._offsets.left;
var y = this._center[1] + Math.sin(avgang) * fact + this.canvas._offsets.top;
var labelelem = $('<div class="jqplot-pie-series jqplot-data-label" style="position:absolute;">' + label + '</div>').insertBefore(plot.eventCanvas._elem);
if (this.dataLabelCenterOn) {
x -= labelelem.width()/2;
y -= labelelem.height()/2;
}
else {
x -= labelelem.width() * Math.sin(avgang/2);
y -= labelelem.height()/2;
}
x = Math.round(x);
y = Math.round(y);
labelelem.css({left: x, top: y});
}
}
}
};
$.jqplot.PieAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
};
$.jqplot.PieAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.PieAxisRenderer.prototype.constructor = $.jqplot.PieAxisRenderer;
// There are no traditional axes on a pie chart. We just need to provide
// dummy objects with properties so the plot will render.
// called with scope of axis object.
$.jqplot.PieAxisRenderer.prototype.init = function(options){
//
this.tickRenderer = $.jqplot.PieTickRenderer;
$.extend(true, this, options);
// I don't think I'm going to need _dataBounds here.
// have to go Axis scaling in a way to fit chart onto plot area
// and provide u2p and p2u functionality for mouse cursor, etc.
// for convienence set _dataBounds to 0 and 100 and
// set min/max to 0 and 100.
this._dataBounds = {min:0, max:100};
this.min = 0;
this.max = 100;
this.showTicks = false;
this.ticks = [];
this.showMark = false;
this.show = false;
};
$.jqplot.PieLegendRenderer = function(){
$.jqplot.TableLegendRenderer.call(this);
};
$.jqplot.PieLegendRenderer.prototype = new $.jqplot.TableLegendRenderer();
$.jqplot.PieLegendRenderer.prototype.constructor = $.jqplot.PieLegendRenderer;
/**
* Class: $.jqplot.PieLegendRenderer
* Legend Renderer specific to pie plots. Set by default
* when user creates a pie plot.
*/
$.jqplot.PieLegendRenderer.prototype.init = function(options) {
// Group: Properties
//
// prop: numberRows
// Maximum number of rows in the legend. 0 or null for unlimited.
this.numberRows = null;
// prop: numberColumns
// Maximum number of columns in the legend. 0 or null for unlimited.
this.numberColumns = null;
// prop: width
// Fixed with of legend. 0 or null for auto size
this.width = null;
$.extend(true, this, options);
};
// called with context of legend
$.jqplot.PieLegendRenderer.prototype.draw = function() {
var legend = this;
if (this.show) {
var series = this._series;
this._elem = $(document.createElement('table'));
this._elem.addClass('jqplot-table-legend');
var ss = {position:'absolute'};
if (this.background) {
ss['background'] = this.background;
}
if (this.border) {
ss['border'] = this.border;
}
if (this.fontSize) {
ss['fontSize'] = this.fontSize;
}
if (this.fontFamily) {
ss['fontFamily'] = this.fontFamily;
}
if (this.textColor) {
ss['textColor'] = this.textColor;
}
if (this.marginTop != null) {
ss['marginTop'] = this.marginTop;
}
if (this.marginBottom != null) {
ss['marginBottom'] = this.marginBottom;
}
if (this.marginLeft != null) {
ss['marginLeft'] = this.marginLeft;
}
if (this.marginRight != null) {
ss['marginRight'] = this.marginRight;
}
this._elem.css(ss);
// Pie charts legends don't go by number of series, but by number of data points
// in the series. Refactor things here for that.
var pad = false,
reverse = false,
nr,
nc;
var s = series[0];
var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
if (s.show) {
var pd = s.data;
if (this.numberRows) {
nr = this.numberRows;
if (!this.numberColumns){
nc = Math.ceil(pd.length/nr);
}
else{
nc = this.numberColumns;
}
}
else if (this.numberColumns) {
nc = this.numberColumns;
nr = Math.ceil(pd.length/this.numberColumns);
}
else {
nr = pd.length;
nc = 1;
}
var i, j;
var tr, td1, td2;
var lt, tt, rs, color;
var idx = 0;
var div0, div1;
for (i=0; i<nr; i++) {
tr = $(document.createElement('tr'));
tr.addClass('jqplot-table-legend');
if (reverse){
tr.prependTo(this._elem);
}
else{
tr.appendTo(this._elem);
}
for (j=0; j<nc; j++) {
if (idx < pd.length) {
tt = '';
if (this.labels[idx]) {
lt = this.labels[idx];
}
else {
if (typeof pd[idx][0] === 'object') {
lt = pd[idx][0][0].toString();
tt = pd[idx][0][1].toString();
}
else {
lt = pd[idx][0].toString();
}
}
//lt = this.labels[idx] || pd[idx][0].toString();
color = colorGenerator.next();
if (!reverse){
if (i>0){
pad = true;
}
else{
pad = false;
}
}
else{
if (i == nr -1){
pad = false;
}
else{
pad = true;
}
}
rs = (pad) ? this.rowSpacing : '0';
td1 = $(document.createElement('td'));
td1.addClass('jqplot-table-legend jqplot-table-legend-swatch');
td1.css({textAlign: 'center', paddingTop: rs});
div0 = $(document.createElement('div'));
div0.addClass('jqplot-table-legend-swatch-outline');
if (tt !== '') {
div0.attr("title", tt);
}
div1 = $(document.createElement('div'));
div1.addClass('jqplot-table-legend-swatch');
div1.css({backgroundColor: color, borderColor: color});
td1.append(div0.append(div1));
td2 = $(document.createElement('td'));
td2.addClass('jqplot-table-legend jqplot-table-legend-label');
td2.css('paddingTop', rs);
if (this.escapeHtml){
td2.text(lt);
}
else {
td2.html('<a title="' + tt + '">' + lt + "</a>");
}
if (reverse) {
td2.prependTo(tr);
td1.prependTo(tr);
}
else {
td1.appendTo(tr);
td2.appendTo(tr);
}
pad = true;
}
idx++;
}
}
}
}
return this._elem;
};
$.jqplot.PieRenderer.prototype.handleMove = function(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
plot.target.trigger('jqplotDataMouseOver', ins);
if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.pieRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
plot.target.trigger('jqplotDataHighlight', ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
};
// this.eventCanvas._elem.bind($.jqplot.eventListenerHooks[i][0], {plot:this}, $.jqplot.eventListenerHooks[i][1]);
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = options.axesDefaults || {};
options.legend = options.legend || {};
options.seriesDefaults = options.seriesDefaults || {};
// only set these if there is a pie series
var setopts = false;
if (options.seriesDefaults.renderer == $.jqplot.PieRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer == $.jqplot.PieRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.PieAxisRenderer;
options.legend.renderer = options.legend.renderer || $.jqplot.PieLegendRenderer;
options.legend.preDraw = true;
options.seriesDefaults.pointLabels = {show: false};
}
}
function postInit(target, data, options) {
for (var i=0; i<this.series.length; i++) {
if (this.series[i].renderer.constructor == $.jqplot.PieRenderer) {
// don't allow mouseover and mousedown at same time.
if (this.series[i].highlightMouseOver) {
this.series[i].highlightMouseDown = false;
}
}
}
}
// called with scope of plot
function postParseOptions(options) {
for (var i=0; i<this.series.length; i++) {
this.series[i].seriesColors = this.seriesColors;
this.series[i].colorGenerator = $.jqplot.colorGenerator;
}
}
function highlight (plot, sidx, pidx) {
if (plot.series[sidx].showSlice[pidx]) {
var s = plot.series[sidx];
var canvas = plot.plugins.pieRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
s._highlightedPoint = pidx;
plot.plugins.pieRenderer.highlightedSeriesIndex = sidx;
s.renderer.drawSlice.call(s, canvas._ctx, s._sliceAngles[pidx][0], s._sliceAngles[pidx][1], s.highlightColorGenerator.get(pidx), false);
}
}
function unhighlight (plot) {
var canvas = plot.plugins.pieRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
for (var i=0; i<plot.series.length; i++) {
plot.series[i]._highlightedPoint = null;
}
plot.plugins.pieRenderer.highlightedSeriesIndex = null;
plot.target.trigger('jqplotDataUnhighlight');
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt1 = jQuery.Event('jqplotDataMouseOver');
evt1.pageX = ev.pageX;
evt1.pageY = ev.pageY;
plot.target.trigger(evt1, ins);
if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.pieRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseDown(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
if (plot.series[ins[0]].highlightMouseDown && !(ins[0] == plot.plugins.pieRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, ins[0], ins[1]);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
function handleMouseUp(ev, gridpos, datapos, neighbor, plot) {
var idx = plot.plugins.pieRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
}
function handleClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt = jQuery.Event('jqplotDataClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
function handleRightClick(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var idx = plot.plugins.pieRenderer.highlightedSeriesIndex;
if (idx != null && plot.series[idx].highlightMouseDown) {
unhighlight(plot);
}
var evt = jQuery.Event('jqplotDataRightClick');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
}
}
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
function postPlotDraw() {
// Memory Leaks patch
if (this.plugins.pieRenderer && this.plugins.pieRenderer.highlightCanvas) {
this.plugins.pieRenderer.highlightCanvas.resetCanvas();
this.plugins.pieRenderer.highlightCanvas = null;
}
this.plugins.pieRenderer = {highlightedSeriesIndex:null};
this.plugins.pieRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
// do we have any data labels? if so, put highlight canvas before those
var labels = $(this.targetId+' .jqplot-data-label');
if (labels.length) {
$(labels[0]).before(this.plugins.pieRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-pieRenderer-highlight-canvas', this._plotDimensions, this));
}
// else put highlight canvas before event canvas.
else {
this.eventCanvas._elem.before(this.plugins.pieRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-pieRenderer-highlight-canvas', this._plotDimensions, this));
}
var hctx = this.plugins.pieRenderer.highlightCanvas.setContext();
this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
}
$.jqplot.preInitHooks.push(preInit);
$.jqplot.PieTickRenderer = function() {
$.jqplot.AxisTickRenderer.call(this);
};
$.jqplot.PieTickRenderer.prototype = new $.jqplot.AxisTickRenderer();
$.jqplot.PieTickRenderer.prototype.constructor = $.jqplot.PieTickRenderer;
})(jQuery);

View File

@@ -0,0 +1,379 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.PointLabels
* Plugin for putting labels at the data points.
*
* To use this plugin, include the js
* file in your source:
*
* > <script type="text/javascript" src="plugins/jqplot.pointLabels.js"></script>
*
* By default, the last value in the data ponit array in the data series is used
* for the label. For most series renderers, extra data can be added to the
* data point arrays and the last value will be used as the label.
*
* For instance,
* this series:
*
* > [[1,4], [3,5], [7,2]]
*
* Would, by default, use the y values in the labels.
* Extra data can be added to the series like so:
*
* > [[1,4,'mid'], [3 5,'hi'], [7,2,'low']]
*
* And now the point labels would be 'mid', 'low', and 'hi'.
*
* Options to the point labels and a custom labels array can be passed into the
* "pointLabels" option on the series option like so:
*
* > series:[{pointLabels:{
* > labels:['mid', 'hi', 'low'],
* > location:'se',
* > ypadding: 12
* > }
* > }]
*
* A custom labels array in the options takes precendence over any labels
* in the series data. If you have a custom labels array in the options,
* but still want to use values from the series array as labels, set the
* "labelsFromSeries" option to true.
*
* By default, html entities (<, >, etc.) are escaped in point labels.
* If you want to include actual html markup in the labels,
* set the "escapeHTML" option to false.
*
*/
$.jqplot.PointLabels = function(options) {
// Group: Properties
//
// prop: show
// show the labels or not.
this.show = $.jqplot.config.enablePlugins;
// prop: location
// compass location where to position the label around the point.
// 'n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'
this.location = 'n';
// prop: labelsFromSeries
// true to use labels within data point arrays.
this.labelsFromSeries = false;
// prop: seriesLabelIndex
// array index for location of labels within data point arrays.
// if null, will use the last element of the data point array.
this.seriesLabelIndex = null;
// prop: labels
// array of arrays of labels, one array for each series.
this.labels = [];
// actual labels that will get displayed.
// needed to preserve user specified labels in labels array.
this._labels = [];
// prop: stackedValue
// true to display value as stacked in a stacked plot.
// no effect if labels is specified.
this.stackedValue = false;
// prop: ypadding
// vertical padding in pixels between point and label
this.ypadding = 6;
// prop: xpadding
// horizontal padding in pixels between point and label
this.xpadding = 6;
// prop: escapeHTML
// true to escape html entities in the labels.
// If you want to include markup in the labels, set to false.
this.escapeHTML = true;
// prop: edgeTolerance
// Number of pixels that the label must be away from an axis
// boundary in order to be drawn. Negative values will allow overlap
// with the grid boundaries.
this.edgeTolerance = -5;
// prop: formatter
// A class of a formatter for the tick text. sprintf by default.
this.formatter = $.jqplot.DefaultTickFormatter;
// prop: formatString
// string passed to the formatter.
this.formatString = '';
// prop: hideZeros
// true to not show a label for a value which is 0.
this.hideZeros = false;
this._elems = [];
$.extend(true, this, options);
};
var locations = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'];
var locationIndicies = {'nw':0, 'n':1, 'ne':2, 'e':3, 'se':4, 's':5, 'sw':6, 'w':7};
var oppositeLocations = ['se', 's', 'sw', 'w', 'nw', 'n', 'ne', 'e'];
// called with scope of a series
$.jqplot.PointLabels.init = function (target, data, seriesDefaults, opts, plot){
var options = $.extend(true, {}, seriesDefaults, opts);
options.pointLabels = options.pointLabels || {};
if (this.renderer.constructor === $.jqplot.BarRenderer && this.barDirection === 'horizontal' && !options.pointLabels.location) {
options.pointLabels.location = 'e';
}
// add a pointLabels attribute to the series plugins
this.plugins.pointLabels = new $.jqplot.PointLabels(options.pointLabels);
this.plugins.pointLabels.setLabels.call(this);
};
// called with scope of series
$.jqplot.PointLabels.prototype.setLabels = function() {
var p = this.plugins.pointLabels;
var labelIdx;
if (p.seriesLabelIndex != null) {
labelIdx = p.seriesLabelIndex;
}
else if (this.renderer.constructor === $.jqplot.BarRenderer && this.barDirection === 'horizontal') {
labelIdx = (this._plotData[0].length < 3) ? 0 : this._plotData[0].length -1;
}
else {
labelIdx = (this._plotData.length === 0) ? 0 : this._plotData[0].length -1;
}
p._labels = [];
if (p.labels.length === 0 || p.labelsFromSeries) {
if (p.stackedValue) {
if (this._plotData.length && this._plotData[0].length){
// var idx = p.seriesLabelIndex || this._plotData[0].length -1;
for (var i=0; i<this._plotData.length; i++) {
p._labels.push(this._plotData[i][labelIdx]);
}
}
}
else {
// var d = this._plotData;
var d = this.data;
if (this.renderer.constructor === $.jqplot.BarRenderer && this.waterfall) {
d = this._data;
}
if (d.length && d[0].length) {
// var idx = p.seriesLabelIndex || d[0].length -1;
for (var i=0; i<d.length; i++) {
p._labels.push(d[i][labelIdx]);
}
}
d = null;
}
}
else if (p.labels.length){
p._labels = p.labels;
}
};
$.jqplot.PointLabels.prototype.xOffset = function(elem, location, padding) {
location = location || this.location;
padding = padding || this.xpadding;
var offset;
switch (location) {
case 'nw':
offset = -elem.outerWidth(true) - this.xpadding;
break;
case 'n':
offset = -elem.outerWidth(true)/2;
break;
case 'ne':
offset = this.xpadding;
break;
case 'e':
offset = this.xpadding;
break;
case 'se':
offset = this.xpadding;
break;
case 's':
offset = -elem.outerWidth(true)/2;
break;
case 'sw':
offset = -elem.outerWidth(true) - this.xpadding;
break;
case 'w':
offset = -elem.outerWidth(true) - this.xpadding;
break;
default: // same as 'nw'
offset = -elem.outerWidth(true) - this.xpadding;
break;
}
return offset;
};
$.jqplot.PointLabels.prototype.yOffset = function(elem, location, padding) {
location = location || this.location;
padding = padding || this.xpadding;
var offset;
switch (location) {
case 'nw':
offset = -elem.outerHeight(true) - this.ypadding;
break;
case 'n':
offset = -elem.outerHeight(true) - this.ypadding;
break;
case 'ne':
offset = -elem.outerHeight(true) - this.ypadding;
break;
case 'e':
offset = -elem.outerHeight(true)/2;
break;
case 'se':
offset = this.ypadding;
break;
case 's':
offset = this.ypadding;
break;
case 'sw':
offset = this.ypadding;
break;
case 'w':
offset = -elem.outerHeight(true)/2;
break;
default: // same as 'nw'
offset = -elem.outerHeight(true) - this.ypadding;
break;
}
return offset;
};
// called with scope of series
$.jqplot.PointLabels.draw = function (sctx, options, plot) {
var p = this.plugins.pointLabels;
// set labels again in case they have changed.
p.setLabels.call(this);
// remove any previous labels
for (var i=0; i<p._elems.length; i++) {
// Memory Leaks patch
// p._elems[i].remove();
if(p._elems[i]) {
p._elems[i].emptyForce();
}
}
p._elems.splice(0, p._elems.length);
if (p.show) {
var ax = '_'+this._stackAxis+'axis';
if (!p.formatString) {
p.formatString = this[ax]._ticks[0].formatString;
p.formatter = this[ax]._ticks[0].formatter;
}
var pd = this._plotData;
var ppd = this._prevPlotData;
var xax = this._xaxis;
var yax = this._yaxis;
var elem, helem;
for (var i=0, l=p._labels.length; i < l; i++) {
var label = p._labels[i];
if (label == null || (p.hideZeros && parseFloat(label) == 0)) {
continue;
}
label = p.formatter(p.formatString, label);
helem = document.createElement('div');
p._elems[i] = $(helem);
elem = p._elems[i];
elem.addClass('jqplot-point-label jqplot-series-'+this.index+' jqplot-point-'+i);
elem.css('position', 'absolute');
elem.insertAfter(sctx.canvas);
if (p.escapeHTML) {
elem.text(label);
}
else {
elem.html(label);
}
var location = p.location;
if ((this.fillToZero && pd[i][1] < 0) || (this.fillToZero && this._type === 'bar' && this.barDirection === 'horizontal' && pd[i][0] < 0) || (this.waterfall && parseInt(label, 10)) < 0) {
location = oppositeLocations[locationIndicies[location]];
}
var ell = xax.u2p(pd[i][0]) + p.xOffset(elem, location);
var elt = yax.u2p(pd[i][1]) + p.yOffset(elem, location);
// we have stacked chart but are not showing stacked values,
// place labels in center.
if (this._stack && !p.stackedValue) {
if (this.barDirection === "vertical") {
elt = (this._barPoints[i][0][1] + this._barPoints[i][1][1]) / 2 + plot._gridPadding.top - 0.5 * elem.outerHeight(true);
}
else {
ell = (this._barPoints[i][2][0] + this._barPoints[i][0][0]) / 2 + plot._gridPadding.left - 0.5 * elem.outerWidth(true);
}
}
if (this.renderer.constructor == $.jqplot.BarRenderer) {
if (this.barDirection == "vertical") {
ell += this._barNudge;
}
else {
elt -= this._barNudge;
}
}
elem.css('left', ell);
elem.css('top', elt);
var elr = ell + elem.width();
var elb = elt + elem.height();
var et = p.edgeTolerance;
var scl = $(sctx.canvas).position().left;
var sct = $(sctx.canvas).position().top;
var scr = sctx.canvas.width + scl;
var scb = sctx.canvas.height + sct;
// if label is outside of allowed area, remove it
if (ell - et < scl || elt - et < sct || elr + et > scr || elb + et > scb) {
elem.remove();
}
elem = null;
helem = null;
}
// finally, animate them if the series is animated
// if (this.renderer.animation && this.renderer.animation._supported && this.renderer.animation.show && plot._drawCount < 2) {
// var sel = '.jqplot-point-label.jqplot-series-'+this.index;
// $(sel).hide();
// $(sel).fadeIn(1000);
// }
}
};
$.jqplot.postSeriesInitHooks.push($.jqplot.PointLabels.init);
$.jqplot.postDrawSeriesHooks.push($.jqplot.PointLabels.draw);
})(jQuery);

View File

@@ -0,0 +1,728 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
$.jqplot.PyramidAxisRenderer = function() {
$.jqplot.LinearAxisRenderer.call(this);
};
$.jqplot.PyramidAxisRenderer.prototype = new $.jqplot.LinearAxisRenderer();
$.jqplot.PyramidAxisRenderer.prototype.constructor = $.jqplot.PyramidAxisRenderer;
// called with scope of axis
$.jqplot.PyramidAxisRenderer.prototype.init = function(options){
// Group: Properties
//
// prop: position
// Position of axis. Values are: top, bottom , left, center, right.
// By default, x and x2 axes are bottom, y axis is center.
this.position = null;
// prop: drawBaseline
// True to draw the axis baseline.
this.drawBaseline = true;
// prop: baselineWidth
// width of the baseline in pixels.
this.baselineWidth = null;
// prop: baselineColor
// CSS color spec for the baseline.
this.baselineColor = null;
this.tickSpacingFactor = 25;
this._type = 'pyramid';
this._splitAxis = false;
this._splitLength = null;
this.category = false;
this._autoFormatString = '';
this._overrideFormatString = false;
$.extend(true, this, options);
this.renderer.options = options;
this.resetDataBounds = this.renderer.resetDataBounds;
this.resetDataBounds();
};
$.jqplot.PyramidAxisRenderer.prototype.resetDataBounds = function() {
// Go through all the series attached to this axis and find
// the min/max bounds for this axis.
var db = this._dataBounds;
db.min = null;
db.max = null;
var temp;
for (var i=0; i<this._series.length; i++) {
var s = this._series[i];
var d = s._plotData;
for (var j=0, l=d.length; j<l; j++) {
if (this.name.charAt(0) === 'x') {
temp = d[j][1];
if ((temp !== null && temp < db.min) || db.min === null) {
db.min = temp;
}
if ((temp !== null && temp > db.max) || db.max === null) {
db.max = temp;
}
}
else {
temp = d[j][0];
if ((temp !== null && temp < db.min) || db.min === null) {
db.min = temp;
}
if ((temp !== null && temp > db.max) || db.max === null) {
db.max = temp;
}
}
}
}
};
// called with scope of axis
$.jqplot.PyramidAxisRenderer.prototype.draw = function(ctx, plot) {
if (this.show) {
// populate the axis label and value properties.
// createTicks is a method on the renderer, but
// call it within the scope of the axis.
this.renderer.createTicks.call(this, plot);
// fill a div with axes labels in the right direction.
// Need to pregenerate each axis to get its bounds and
// position it and the labels correctly on the plot.
var dim=0;
var temp;
// Added for theming.
if (this._elem) {
// Memory Leaks patch
//this._elem.empty();
this._elem.emptyForce();
this._elem = null;
}
this._elem = $(document.createElement('div'));
this._elem.addClass('jqplot-axis jqplot-'+this.name);
this._elem.css('position', 'absolute');
if (this.name == 'xaxis' || this.name == 'x2axis') {
this._elem.width(this._plotDimensions.width);
}
else {
this._elem.height(this._plotDimensions.height);
}
// create a _label object.
this.labelOptions.axis = this.name;
this._label = new this.labelRenderer(this.labelOptions);
if (this._label.show) {
var elem = this._label.draw(ctx, plot);
elem.appendTo(this._elem);
elem = null;
}
var t = this._ticks;
var tick;
for (var i=0; i<t.length; i++) {
tick = t[i];
if (tick.show && tick.showLabel && (!tick.isMinorTick)) {
this._elem.append(tick.draw(ctx, plot));
}
}
tick = null;
t = null;
}
return this._elem;
};
// Note, primes can be found on http://primes.utm.edu/
var _primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983, 991, 997];
var _primesHash = {};
for (var i =0, l = _primes.length; i < l; i++) {
_primesHash[_primes[i]] = _primes[i];
}
// called with scope of axis
$.jqplot.PyramidAxisRenderer.prototype.createTicks = function(plot) {
// we're are operating on an axis here
var userTicks = this.ticks;
// databounds were set on axis initialization.
var db = this._dataBounds;
var dim;
var interval;
var min;
var max;
var range;
var pos1;
var pos2;
var tt;
var i;
var l;
var s;
// get a copy of user's settings for min/max.
var userMin = this.min;
var userMax = this.max;
var ut;
var t;
var threshold;
var tdim;
var scalefact;
var ret;
var tumin;
var tumax;
var maxVisibleTicks;
var val;
var skip = null;
var temp;
// if we already have ticks, use them.
// ticks must be in order of increasing value.
if (userTicks.length) {
// ticks could be 1D or 2D array of [val, val, ,,,] or [[val, label], [val, label], ...] or mixed
for (i=0, l=userTicks.length; i<l; i++){
ut = userTicks[i];
t = new this.tickRenderer(this.tickOptions);
if ($.isArray(ut)) {
t.value = ut[0];
t.label = ut[1];
t.setTick(ut[0], this.name);
this._ticks.push(t);
}
else if ($.isPlainObject(ut)) {
$.extend(true, t, ut);
t.axis = this.name;
this._ticks.push(t);
}
else {
if (typeof ut === 'string') {
val = i + plot.defaultAxisStart;
}
else {
val = ut;
}
t.value = val;
t.label = ut;
t.axis = this.name;
this._ticks.push(t);
}
}
this.numberTicks = userTicks.length;
this.min = this._ticks[0].value;
this.max = this._ticks[this.numberTicks-1].value;
this.tickInterval = (this.max - this.min) / (this.numberTicks - 1);
// use user specified tickInterval if there is one
if (this._options.tickInterval) {
// hide every tick except for ticks on interval
var ti = this._options.tickInterval;
for (i=0; i<this.numberTicks; i++) {
if (i%ti !== 0) {
// this._ticks[i].show = false;
this._ticks[i].isMinorTick = true;
}
}
}
else {
// check if we have too many ticks
dim = (this.name.charAt(0) === 'x') ? this._plotDimensions.width : this._plotDimensions.height;
maxVisibleTicks = Math.round(2.0 + dim/this.tickSpacingFactor);
if (this.numberTicks > maxVisibleTicks) {
// check for number of ticks we can skip
temp = this.numberTicks - 1;
for (i=2; i<temp; i++) {
if (temp % i === 0 && temp/i < maxVisibleTicks) {
skip = i-1;
break;
}
}
if (skip !== null) {
var count = 1;
for (i=1, l=this._ticks.length; i<l; i++) {
if (count <= skip) {
this._ticks[i].show = false;
count += 1;
}
else {
count = 1;
}
}
}
}
}
// if category style, add minor ticks in between
temp = [];
if (this.category) {
// turn off gridline and mark on first tick
this._ticks[0].showGridline = false;
this._ticks[0].showMark = false;
for (i=this._ticks.length-1; i>0; i--) {
t = new this.tickRenderer(this.tickOptions);
t.value = this._ticks[i-1].value + this.tickInterval/2.0;
t.label = '';
t.showLabel = false;
t.axis = this.name;
this._ticks[i].showGridline = false;
this._ticks[i].showMark = false;
this._ticks.splice(i, 0, t);
// temp.push(t);
}
// merge in the new ticks
// for (i=1, l=temp.length; i<l; i++) {
// this._ticks.splice(i, 0, temp[i]);
// }
// now add a tick at beginning and end
t = new this.tickRenderer(this.tickOptions);
t.value = this._ticks[0].value - this.tickInterval/2.0;
t.label = '';
t.showLabel = false;
t.axis = this.name;
this._ticks.unshift(t);
t = new this.tickRenderer(this.tickOptions);
t.value = this._ticks[this._ticks.length-1].value + this.tickInterval/2.0;
t.label = '';
t.showLabel = false;
t.axis = this.name;
this._ticks.push(t);
this.tickInterval = this.tickInterval / 2.0;
this.numberTicks = this._ticks.length;
this.min = this._ticks[0].value;
this.max = this._ticks[this._ticks.length-1].value;
}
}
// we don't have any ticks yet, let's make some!
else {
if (this.name.charAt(0) === 'x') {
dim = this._plotDimensions.width;
// make sure x axis is symetric about 0.
var tempmax = Math.max(db.max, Math.abs(db.min));
var tempmin = Math.min(db.min, -tempmax);
// min = ((this.min != null) ? this.min : tempmin);
// max = ((this.max != null) ? this.max : tempmax);
min = tempmin;
max = tempmax;
range = max - min;
if (this.tickOptions == null || !this.tickOptions.formatString) {
this._overrideFormatString = true;
}
threshold = 30;
tdim = Math.max(dim, threshold+1);
scalefact = (tdim-threshold)/300.0;
ret = $.jqplot.LinearTickGenerator(min, max, scalefact);
// calculate a padded max and min, points should be less than these
// so that they aren't too close to the edges of the plot.
// User can adjust how much padding is allowed with pad, padMin and PadMax options.
tumin = min + range*(this.padMin - 1);
tumax = max - range*(this.padMax - 1);
if (min < tumin || max > tumax) {
tumin = min - range*(this.padMin - 1);
tumax = max + range*(this.padMax - 1);
ret = $.jqplot.LinearTickGenerator(tumin, tumax, scalefact);
}
this.min = ret[0];
this.max = ret[1];
this.numberTicks = ret[2];
this._autoFormatString = ret[3];
this.tickInterval = ret[4];
}
else {
dim = this._plotDimensions.height;
// ticks will be on whole integers like 1, 2, 3, ... or 1, 4, 7, ...
min = db.min;
max = db.max;
s = this._series[0];
this._ticks = [];
range = max - min;
// if range is a prime, will get only 2 ticks, expand range in that case.
if (_primesHash[range]) {
range += 1;
max += 1;
}
this.max = max;
this.min = min;
maxVisibleTicks = Math.round(2.0 + dim/this.tickSpacingFactor);
if (range + 1 <= maxVisibleTicks) {
this.numberTicks = range + 1;
this.tickInterval = 1.0;
}
else {
// figure out a round number of ticks to skip in every interval
// range / ti + 1 = nt
// ti = range / (nt - 1)
for (var i=maxVisibleTicks; i>1; i--) {
if (range/(i - 1) === Math.round(range/(i - 1))) {
this.numberTicks = i;
this.tickInterval = range/(i - 1);
break;
}
}
}
}
if (this._overrideFormatString && this._autoFormatString != '') {
this.tickOptions = this.tickOptions || {};
this.tickOptions.formatString = this._autoFormatString;
}
var labelval;
for (i=0; i<this.numberTicks; i++) {
this.tickOptions.axis = this.name;
labelval = this.min + this.tickInterval * i;
if (this.name.charAt(0) === 'x') {
labelval = Math.abs(labelval);
}
// this.tickOptions.label = String (labelval);
this.tickOptions.value = this.min + this.tickInterval * i;
t = new this.tickRenderer(this.tickOptions);
t.label = t.prefix + t.formatter(t.formatString, labelval);
this._ticks.push(t);
// for x axis, if y axis is in middle, add a symetrical 0 tick
if (this.name.charAt(0) === 'x' && plot.axes.yMidAxis.show && this.tickOptions.value === 0) {
this._splitAxis = true;
this._splitLength = plot.axes.yMidAxis.getWidth();
// t.value = -this.max/2000.0;
t = new this.tickRenderer(this.tickOptions);
this._ticks.push(t);
t.value = this.max/2000.0;
}
}
t = null;
}
};
// called with scope of axis
$.jqplot.PyramidAxisRenderer.prototype.set = function() {
var dim = 0;
var temp;
var w = 0;
var h = 0;
var i;
var t;
var tick;
var lshow = (this._label == null) ? false : this._label.show;
if (this.show) {
t = this._ticks;
l = t.length;
for (i=0; i<l; i++) {
tick = t[i];
if (!tick._breakTick && tick.show && tick.showLabel && !tick.isMinorTick) {
if (this.name.charAt(0) === 'x') {
temp = tick._elem.outerHeight(true);
}
else {
temp = tick._elem.outerWidth(true);
}
if (temp > dim) {
dim = temp;
}
}
}
if (this.name === 'yMidAxis') {
for (i=0; i<l; i++) {
tick = t[i];
if (tick._elem) {
temp = (dim - tick._elem.outerWidth(true))/2.0;
tick._elem.css('left', temp);
}
}
}
tick = null;
t = null;
if (lshow) {
w = this._label._elem.outerWidth(true);
h = this._label._elem.outerHeight(true);
}
if (this.name === 'xaxis') {
dim = dim + h;
this._elem.css({'height':dim+'px', left:'0px', bottom:'0px'});
}
else if (this.name === 'x2axis') {
dim = dim + h;
this._elem.css({'height':dim+'px', left:'0px', top:'0px'});
}
else if (this.name === 'yaxis') {
dim = dim + w;
this._elem.css({'width':dim+'px', left:'0px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css('width', w+'px');
}
}
else if (this.name === 'yMidAxis') {
// don't include width of label at all in width of axis?
// dim = (dim > w) ? dim : w;
var temp = dim/2.0 - w/2.0;
this._elem.css({'width':dim+'px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css({width: w, left: temp, top: 0});
}
}
else {
dim = dim + w;
this._elem.css({'width':dim+'px', right:'0px', top:'0px'});
if (lshow && this._label.constructor == $.jqplot.AxisLabelRenderer) {
this._label._elem.css('width', w+'px');
}
}
}
};
$.jqplot.PyramidAxisRenderer.prototype.pack = function(pos, offsets) {
// Add defaults for repacking from resetTickValues function.
pos = pos || {};
offsets = offsets || this._offsets;
var ticks = this._ticks;
var max = this.max;
var min = this.min;
var offmax = offsets.max;
var offmin = offsets.min;
var lshow = (this._label == null) ? false : this._label.show;
for (var p in pos) {
this._elem.css(p, pos[p]);
}
this._offsets = offsets;
// pixellength will be + for x axes and - for y axes becasue pixels always measured from top left.
var pixellength = offmax - offmin;
var unitlength = max - min;
var sl = this._splitLength;
// point to unit and unit to point conversions references to Plot DOM element top left corner.
if (this._splitAxis) {
pixellength -= this._splitLength;
// don't know that this one is correct.
this.p2u = function(p){
return (p - offmin) * unitlength / pixellength + min;
};
this.u2p = function(u){
if (u <= 0) {
return (u - min) * pixellength / unitlength + offmin;
}
else {
return (u - min) * pixellength / unitlength + offmin + sl;
}
};
this.series_u2p = function(u){
if (u <= 0) {
return (u - min) * pixellength / unitlength;
}
else {
return (u - min) * pixellength / unitlength + sl;
}
};
// don't know that this one is correct.
this.series_p2u = function(p){
return p * unitlength / pixellength + min;
};
}
else {
this.p2u = function(p){
return (p - offmin) * unitlength / pixellength + min;
};
this.u2p = function(u){
return (u - min) * pixellength / unitlength + offmin;
};
if (this.name.charAt(0) === 'x'){
this.series_u2p = function(u){
return (u - min) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + min;
};
}
else {
this.series_u2p = function(u){
return (u - max) * pixellength / unitlength;
};
this.series_p2u = function(p){
return p * unitlength / pixellength + max;
};
}
}
if (this.show) {
if (this.name.charAt(0) === 'x') {
for (var i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
// will need to adjust auto positioning based on which axis this is.
var temp = (this.name == 'xaxis') ? 1 : -1;
switch (t.labelPosition) {
case 'auto':
// position at end
if (temp * t.angle < 0) {
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
}
// position at start
else {
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
}
break;
case 'end':
shim = -t.getWidth() + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
case 'start':
shim = -t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
break;
case 'middle':
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
default:
shim = -t.getWidth()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
break;
}
}
else {
shim = -t.getWidth()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('left', val);
t.pack();
}
}
if (lshow) {
var w = this._label._elem.outerWidth(true);
this._label._elem.css('left', offmin + pixellength/2 - w/2 + 'px');
if (this.name == 'xaxis') {
this._label._elem.css('bottom', '0px');
}
else {
this._label._elem.css('top', '0px');
}
this._label.pack();
}
}
else {
for (var i=0; i<ticks.length; i++) {
var t = ticks[i];
if (t.show && t.showLabel && !t.isMinorTick) {
var shim;
if (t.constructor == $.jqplot.CanvasAxisTickRenderer && t.angle) {
// will need to adjust auto positioning based on which axis this is.
var temp = (this.name == 'yaxis') ? 1 : -1;
switch (t.labelPosition) {
case 'auto':
// position at end
case 'end':
if (temp * t.angle < 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'start':
if (t.angle > 0) {
shim = -t._textRenderer.height * Math.cos(-t._textRenderer.angle) / 2;
}
else {
shim = -t.getHeight() + t._textRenderer.height * Math.cos(t._textRenderer.angle) / 2;
}
break;
case 'middle':
// if (t.angle > 0) {
// shim = -t.getHeight()/2 + t._textRenderer.height * Math.sin(-t._textRenderer.angle) / 2;
// }
// else {
// shim = -t.getHeight()/2 - t._textRenderer.height * Math.sin(t._textRenderer.angle) / 2;
// }
shim = -t.getHeight()/2;
break;
default:
shim = -t.getHeight()/2;
break;
}
}
else {
shim = -t.getHeight()/2;
}
var val = this.u2p(t.value) + shim + 'px';
t._elem.css('top', val);
t.pack();
}
}
if (lshow) {
var h = this._label._elem.outerHeight(true);
if (this.name !== 'yMidAxis') {
this._label._elem.css('top', offmax - pixellength/2 - h/2 + 'px');
}
if (this.name == 'yaxis') {
this._label._elem.css('left', '0px');
}
else if (this.name !== 'yMidAxis') {
this._label._elem.css('right', '0px');
}
this._label.pack();
}
}
}
ticks = null;
};
})(jQuery);

View File

@@ -0,0 +1,429 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// Class: $.jqplot.CanvasGridRenderer
// The default jqPlot grid renderer, creating a grid on a canvas element.
// The renderer has no additional options beyond the <Grid> class.
$.jqplot.PyramidGridRenderer = function(){
$.jqplot.CanvasGridRenderer.call(this);
};
$.jqplot.PyramidGridRenderer.prototype = new $.jqplot.CanvasGridRenderer();
$.jqplot.PyramidGridRenderer.prototype.constructor = $.jqplot.PyramidGridRenderer;
// called with context of Grid object
$.jqplot.CanvasGridRenderer.prototype.init = function(options) {
this._ctx;
this.plotBands = {
show: false,
color: 'rgb(230, 219, 179)',
axis: 'y',
start: null,
interval: 10
};
$.extend(true, this, options);
// set the shadow renderer options
var sopts = {lineJoin:'miter', lineCap:'round', fill:false, isarc:false, angle:this.shadowAngle, offset:this.shadowOffset, alpha:this.shadowAlpha, depth:this.shadowDepth, lineWidth:this.shadowWidth, closePath:false, strokeStyle:this.shadowColor};
this.renderer.shadowRenderer.init(sopts);
};
$.jqplot.PyramidGridRenderer.prototype.draw = function() {
this._ctx = this._elem.get(0).getContext("2d");
var ctx = this._ctx;
var axes = this._axes;
var xp = axes.xaxis.u2p;
var yp = axes.yMidAxis.u2p;
var xnudge = axes.xaxis.max/1000.0;
var xp0 = xp(0);
var xpn = xp(xnudge);
var ax = ['xaxis', 'yaxis', 'x2axis', 'y2axis','yMidAxis'];
// Add the grid onto the grid canvas. This is the bottom most layer.
ctx.save();
ctx.clearRect(0, 0, this._plotDimensions.width, this._plotDimensions.height);
ctx.fillStyle = this.backgroundColor || this.background;
ctx.fillRect(this._left, this._top, this._width, this._height);
if (this.plotBands.show) {
ctx.save();
var pb = this.plotBands;
ctx.fillStyle = pb.color;
var axis;
var x, y, w, h;
// find axis to work with
if (pb.axis.charAt(0) === 'x') {
if (axes.xaxis.show) {
axis = axes.xaxis;
}
}
else if (pb.axis.charAt(0) === 'y') {
if (axes.yaxis.show) {
axis = axes.yaxis;
}
else if (axes.y2axis.show) {
axis = axes.y2axis;
}
else if (axes.yMidAxis.show) {
axis = axes.yMidAxis;
}
}
if (axis !== undefined) {
// draw some rectangles
var start = pb.start;
if (start === null) {
start = axis.min;
}
for (var i = start; i < axis.max; i += 2 * pb.interval) {
if (axis.name.charAt(0) === 'y') {
x = this._left;
if ((i + pb.interval) < axis.max) {
y = axis.series_u2p(i + pb.interval) + this._top;
}
else {
y = axis.series_u2p(axis.max) + this._top;
}
w = this._right - this._left;
h = axis.series_u2p(start) - axis.series_u2p(start + pb.interval);
ctx.fillRect(x, y, w, h);
}
// else {
// y = 0;
// x = axis.series_u2p(i);
// h = this._height;
// w = axis.series_u2p(start + pb.interval) - axis.series_u2p(start);
// }
}
}
ctx.restore();
}
ctx.save();
ctx.lineJoin = 'miter';
ctx.lineCap = 'butt';
ctx.lineWidth = this.gridLineWidth;
ctx.strokeStyle = this.gridLineColor;
var b, e, s, m;
for (var i=5; i>0; i--) {
var name = ax[i-1];
var axis = axes[name];
var ticks = axis._ticks;
var numticks = ticks.length;
if (axis.show) {
if (axis.drawBaseline) {
var bopts = {};
if (axis.baselineWidth !== null) {
bopts.lineWidth = axis.baselineWidth;
}
if (axis.baselineColor !== null) {
bopts.strokeStyle = axis.baselineColor;
}
switch (name) {
case 'xaxis':
if (axes.yMidAxis.show) {
drawLine (this._left, this._bottom, xp0, this._bottom, bopts);
drawLine (xpn, this._bottom, this._right, this._bottom, bopts);
}
else {
drawLine (this._left, this._bottom, this._right, this._bottom, bopts);
}
break;
case 'yaxis':
drawLine (this._left, this._bottom, this._left, this._top, bopts);
break;
case 'yMidAxis':
drawLine(xp0, this._bottom, xp0, this._top, bopts);
drawLine(xpn, this._bottom, xpn, this._top, bopts);
break;
case 'x2axis':
if (axes.yMidAxis.show) {
drawLine (this._left, this._top, xp0, this._top, bopts);
drawLine (xpn, this._top, this._right, this._top, bopts);
}
else {
drawLine (this._left, this._bottom, this._right, this._bottom, bopts);
}
break;
case 'y2axis':
drawLine (this._right, this._bottom, this._right, this._top, bopts);
break;
}
}
for (var j=numticks; j>0; j--) {
var t = ticks[j-1];
if (t.show) {
var pos = Math.round(axis.u2p(t.value)) + 0.5;
switch (name) {
case 'xaxis':
// draw the grid line if we should
if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
drawLine(pos, this._top, pos, this._bottom);
}
// draw the mark
if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
s = t.markSize;
m = t.mark;
var pos = Math.round(axis.u2p(t.value)) + 0.5;
switch (m) {
case 'outside':
b = this._bottom;
e = this._bottom+s;
break;
case 'inside':
b = this._bottom-s;
e = this._bottom;
break;
case 'cross':
b = this._bottom-s;
e = this._bottom+s;
break;
default:
b = this._bottom;
e = this._bottom+s;
break;
}
// draw the shadow
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [[pos,b],[pos,e]], {lineCap:'butt', lineWidth:this.gridLineWidth, offset:this.gridLineWidth*0.75, depth:2, fill:false, closePath:false});
}
// draw the line
drawLine(pos, b, pos, e);
}
break;
case 'yaxis':
// draw the grid line
if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
drawLine(this._right, pos, this._left, pos);
}
// draw the mark
if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
s = t.markSize;
m = t.mark;
var pos = Math.round(axis.u2p(t.value)) + 0.5;
switch (m) {
case 'outside':
b = this._left-s;
e = this._left;
break;
case 'inside':
b = this._left;
e = this._left+s;
break;
case 'cross':
b = this._left-s;
e = this._left+s;
break;
default:
b = this._left-s;
e = this._left;
break;
}
// draw the shadow
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
}
drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
}
break;
case 'yMidAxis':
// draw the grid line
if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
drawLine(this._left, pos, xp0, pos);
drawLine(xpn, pos, this._right, pos);
}
// draw the mark
if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
s = t.markSize;
m = t.mark;
var pos = Math.round(axis.u2p(t.value)) + 0.5;
b = xp0;
e = xp0 + s;
// draw the shadow
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
}
drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
b = xpn - s;
e = xpn;
// draw the shadow
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
}
drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
}
break;
case 'x2axis':
// draw the grid line
if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
drawLine(pos, this._bottom, pos, this._top);
}
// draw the mark
if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
s = t.markSize;
m = t.mark;
var pos = Math.round(axis.u2p(t.value)) + 0.5;
switch (m) {
case 'outside':
b = this._top-s;
e = this._top;
break;
case 'inside':
b = this._top;
e = this._top+s;
break;
case 'cross':
b = this._top-s;
e = this._top+s;
break;
default:
b = this._top-s;
e = this._top;
break;
}
// draw the shadow
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [[pos,b],[pos,e]], {lineCap:'butt', lineWidth:this.gridLineWidth, offset:this.gridLineWidth*0.75, depth:2, fill:false, closePath:false});
}
drawLine(pos, b, pos, e);
}
break;
case 'y2axis':
// draw the grid line
if (t.showGridline && this.drawGridlines && (!t.isMinorTick || axis.showMinorTicks)) {
drawLine(this._left, pos, this._right, pos);
}
// draw the mark
if (t.showMark && t.mark && (!t.isMinorTick || axis.showMinorTicks)) {
s = t.markSize;
m = t.mark;
var pos = Math.round(axis.u2p(t.value)) + 0.5;
switch (m) {
case 'outside':
b = this._right;
e = this._right+s;
break;
case 'inside':
b = this._right-s;
e = this._right;
break;
case 'cross':
b = this._right-s;
e = this._right+s;
break;
default:
b = this._right;
e = this._right+s;
break;
}
// draw the shadow
if (this.shadow) {
this.renderer.shadowRenderer.draw(ctx, [[b, pos], [e, pos]], {lineCap:'butt', lineWidth:this.gridLineWidth*1.5, offset:this.gridLineWidth*0.75, fill:false, closePath:false});
}
drawLine(b, pos, e, pos, {strokeStyle:axis.borderColor});
}
break;
default:
break;
}
}
}
t = null;
}
axis = null;
ticks = null;
}
ctx.restore();
function drawLine(bx, by, ex, ey, opts) {
ctx.save();
opts = opts || {};
if (opts.lineWidth == null || opts.lineWidth != 0){
$.extend(true, ctx, opts);
ctx.beginPath();
ctx.moveTo(bx, by);
ctx.lineTo(ex, ey);
ctx.stroke();
}
ctx.restore();
}
if (this.shadow) {
if (axes.yMidAxis.show) {
var points = [[this._left, this._bottom], [xp0, this._bottom]];
this.renderer.shadowRenderer.draw(ctx, points);
var points = [[xpn, this._bottom], [this._right, this._bottom], [this._right, this._top]];
this.renderer.shadowRenderer.draw(ctx, points);
var points = [[xp0, this._bottom], [xp0, this._top]];
this.renderer.shadowRenderer.draw(ctx, points);
}
else {
var points = [[this._left, this._bottom], [this._right, this._bottom], [this._right, this._top]];
this.renderer.shadowRenderer.draw(ctx, points);
}
}
// Now draw border around grid. Use axis border definitions. start at
// upper left and go clockwise.
if (this.borderWidth != 0 && this.drawBorder) {
if (axes.yMidAxis.show) {
drawLine (this._left, this._top, xp0, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
drawLine (xpn, this._top, this._right, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
drawLine (this._right, this._top, this._right, this._bottom, {lineCap:'round', strokeStyle:axes.y2axis.borderColor, lineWidth:axes.y2axis.borderWidth});
drawLine (this._right, this._bottom, xpn, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
drawLine (xp0, this._bottom, this._left, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
drawLine (this._left, this._bottom, this._left, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
drawLine (xp0, this._bottom, xp0, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
drawLine (xpn, this._bottom, xpn, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
}
else {
drawLine (this._left, this._top, this._right, this._top, {lineCap:'round', strokeStyle:axes.x2axis.borderColor, lineWidth:axes.x2axis.borderWidth});
drawLine (this._right, this._top, this._right, this._bottom, {lineCap:'round', strokeStyle:axes.y2axis.borderColor, lineWidth:axes.y2axis.borderWidth});
drawLine (this._right, this._bottom, this._left, this._bottom, {lineCap:'round', strokeStyle:axes.xaxis.borderColor, lineWidth:axes.xaxis.borderWidth});
drawLine (this._left, this._bottom, this._left, this._top, {lineCap:'round', strokeStyle:axes.yaxis.borderColor, lineWidth:axes.yaxis.borderWidth});
}
}
// ctx.lineWidth = this.borderWidth;
// ctx.strokeStyle = this.borderColor;
// ctx.strokeRect(this._left, this._top, this._width, this._height);
ctx.restore();
ctx = null;
axes = null;
};
})(jQuery);

View File

@@ -0,0 +1,514 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
// Need to ensure pyramid axis and grid renderers are loaded.
// You should load these with script tags in the html head, that is more efficient
// as the browser will cache the request.
// Note, have to block with synchronous request in order to execute bar renderer code.
if ($.jqplot.PyramidAxisRenderer === undefined) {
$.ajax({
url: $.jqplot.pluginLocation + 'jqplot.pyramidAxisRenderer.js',
dataType: "script",
async: false
});
}
if ($.jqplot.PyramidGridRenderer === undefined) {
$.ajax({
url: $.jqplot.pluginLocation + 'jqplot.pyramidGridRenderer.js',
dataType: "script",
async: false
});
}
$.jqplot.PyramidRenderer = function(){
$.jqplot.LineRenderer.call(this);
};
$.jqplot.PyramidRenderer.prototype = new $.jqplot.LineRenderer();
$.jqplot.PyramidRenderer.prototype.constructor = $.jqplot.PyramidRenderer;
// called with scope of a series
$.jqplot.PyramidRenderer.prototype.init = function(options, plot) {
options = options || {};
this._type = 'pyramid';
// Group: Properties
//
// prop: barPadding
this.barPadding = 10;
this.barWidth = null;
// prop: fill
// True to fill the bars.
this.fill = true;
// prop: highlightMouseOver
// True to highlight slice when moused over.
// This must be false to enable highlightMouseDown to highlight when clicking on a slice.
this.highlightMouseOver = true;
// prop: highlightMouseDown
// True to highlight when a mouse button is pressed over a slice.
// This will be disabled if highlightMouseOver is true.
this.highlightMouseDown = false;
// prop: highlightColors
// an array of colors to use when highlighting a slice.
this.highlightColors = [];
// prop highlightThreshold
// Expand the highlightable region in the x direction.
// E.g. a value of 3 will highlight a bar when the mouse is
// within 3 pixels of the bar in the x direction.
this.highlightThreshold = 2;
// prop: synchronizeHighlight
// Index of another series to highlight when this series is highlighted.
// null or false to not synchronize.
this.synchronizeHighlight = false;
// prop: offsetBars
// False will center bars on their y value.
// True will push bars up by 1/2 bar width to fill between their y values.
// If true, there needs to be 1 more tick than there are bars.
this.offsetBars = false;
// if user has passed in highlightMouseDown option and not set highlightMouseOver, disable highlightMouseOver
if (options.highlightMouseDown && options.highlightMouseOver == null) {
options.highlightMouseOver = false;
}
this.side = 'right';
$.extend(true, this, options);
// if (this.fill === false) {
// this.shadow = false;
// }
if (this.side === 'left') {
this._highlightThreshold = [[-this.highlightThreshold, 0], [-this.highlightThreshold, 0], [0,0], [0,0]];
}
else {
this._highlightThreshold = [[0,0], [0,0], [this.highlightThreshold, 0], [this.highlightThreshold, 0]];
}
this.renderer.options = options;
// index of the currenty highlighted point, if any
this._highlightedPoint = null;
// Array of actual data colors used for each data point.
this._dataColors = [];
this._barPoints = [];
this.fillAxis = 'y';
this._primaryAxis = '_yaxis';
this._xnudge = 0;
// set the shape renderer options
var opts = {lineJoin:'miter', lineCap:'butt', fill:this.fill, fillRect:this.fill, isarc:false, strokeStyle:this.color, fillStyle:this.color, closePath:this.fill, lineWidth: this.lineWidth};
this.renderer.shapeRenderer.init(opts);
// set the shadow renderer options
var shadow_offset = options.shadowOffset;
// set the shadow renderer options
if (shadow_offset == null) {
// scale the shadowOffset to the width of the line.
if (this.lineWidth > 2.5) {
shadow_offset = 1.25 * (1 + (Math.atan((this.lineWidth/2.5))/0.785398163 - 1)*0.6);
// var shadow_offset = this.shadowOffset;
}
// for skinny lines, don't make such a big shadow.
else {
shadow_offset = 1.25 * Math.atan((this.lineWidth/2.5))/0.785398163;
}
}
var sopts = {lineJoin:'miter', lineCap:'butt', fill:this.fill, fillRect:this.fill, isarc:false, angle:this.shadowAngle, offset:shadow_offset, alpha:this.shadowAlpha, depth:this.shadowDepth, closePath:this.fill, lineWidth: this.lineWidth};
this.renderer.shadowRenderer.init(sopts);
plot.postDrawHooks.addOnce(postPlotDraw);
plot.eventListenerHooks.addOnce('jqplotMouseMove', handleMove);
// if this is the left side of pyramid, set y values to negative.
if (this.side === 'left') {
for (var i=0, l=this.data.length; i<l; i++) {
this.data[i][1] = -Math.abs(this.data[i][1]);
}
}
};
// setGridData
// converts the user data values to grid coordinates and stores them
// in the gridData array.
// Called with scope of a series.
$.jqplot.PyramidRenderer.prototype.setGridData = function(plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var data = this._plotData;
var pdata = this._prevPlotData;
this.gridData = [];
this._prevGridData = [];
var l = data.length;
var adjust = false;
var i;
// if any data values are < 0, consider this a negative series
for (i = 0; i < l; i++) {
if (data[i][1] < 0) {
this.side = 'left';
}
}
if (this._yaxis.name === 'yMidAxis' && this.side === 'right') {
this._xnudge = this._xaxis.max/2000.0;
adjust = true;
}
for (i = 0; i < l; i++) {
// if not a line series or if no nulls in data, push the converted point onto the array.
if (data[i][0] != null && data[i][1] != null) {
this.gridData.push([xp(data[i][1]), yp(data[i][0])]);
}
// else if there is a null, preserve it.
else if (data[i][0] == null) {
this.gridData.push([xp(data[i][1]), null]);
}
else if (data[i][1] == null) {
this.gridData.push(null, [yp(data[i][0])]);
}
// finally, adjust x grid data if have to
if (data[i][1] === 0 && adjust) {
this.gridData[i][0] = xp(this._xnudge);
}
}
};
// makeGridData
// converts any arbitrary data values to grid coordinates and
// returns them. This method exists so that plugins can use a series'
// linerenderer to generate grid data points without overwriting the
// grid data associated with that series.
// Called with scope of a series.
$.jqplot.PyramidRenderer.prototype.makeGridData = function(data, plot) {
// recalculate the grid data
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var gd = [];
var l = data.length;
var adjust = false;
var i;
// if any data values are < 0, consider this a negative series
for (i = 0; i < l; i++) {
if (data[i][1] < 0) {
this.side = 'left';
}
}
if (this._yaxis.name === 'yMidAxis' && this.side === 'right') {
this._xnudge = this._xaxis.max/2000.0;
adjust = true;
}
for (i = 0; i < l; i++) {
// if not a line series or if no nulls in data, push the converted point onto the array.
if (data[i][0] != null && data[i][1] != null) {
gd.push([xp(data[i][1]), yp(data[i][0])]);
}
// else if there is a null, preserve it.
else if (data[i][0] == null) {
gd.push([xp(data[i][1]), null]);
}
else if (data[i][1] == null) {
gd.push([null, yp(data[i][0])]);
}
// finally, adjust x grid data if have to
if (data[i][1] === 0 && adjust) {
gd[i][0] = xp(this._xnudge);
}
}
return gd;
};
$.jqplot.PyramidRenderer.prototype.setBarWidth = function() {
// need to know how many data values we have on the approprate axis and figure it out.
var i;
var nvals = 0;
var nseries = 0;
var paxis = this[this._primaryAxis];
var s, series, pos;
nvals = paxis.max - paxis.min;
var nticks = paxis.numberTicks;
var nbins = (nticks-1)/2;
// so, now we have total number of axis values.
var temp = (this.barPadding === 0) ? 1.0 : 0;
if (paxis.name == 'xaxis' || paxis.name == 'x2axis') {
this.barWidth = (paxis._offsets.max - paxis._offsets.min) / nvals - this.barPadding + temp;
}
else {
if (this.fill) {
this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals - this.barPadding + temp;
}
else {
this.barWidth = (paxis._offsets.min - paxis._offsets.max) / nvals;
}
}
};
$.jqplot.PyramidRenderer.prototype.draw = function(ctx, gridData, options) {
var i;
// Ughhh, have to make a copy of options b/c it may be modified later.
var opts = $.extend({}, options);
var shadow = (opts.shadow != undefined) ? opts.shadow : this.shadow;
var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
var fill = (opts.fill != undefined) ? opts.fill : this.fill;
var xp = this._xaxis.series_u2p;
var yp = this._yaxis.series_u2p;
var pointx, pointy;
// clear out data colors.
this._dataColors = [];
this._barPoints = [];
if (this.renderer.options.barWidth == null) {
this.renderer.setBarWidth.call(this);
}
// var temp = this._plotSeriesInfo = this.renderer.calcSeriesNumbers.call(this);
// var nvals = temp[0];
// var nseries = temp[1];
// var pos = temp[2];
var points = [],
w,
h;
// this._barNudge = 0;
if (showLine) {
var negativeColors = new $.jqplot.ColorGenerator(this.negativeSeriesColors);
var positiveColors = new $.jqplot.ColorGenerator(this.seriesColors);
var negativeColor = negativeColors.get(this.index);
if (! this.useNegativeColors) {
negativeColor = opts.fillStyle;
}
var positiveColor = opts.fillStyle;
var base;
var xstart = this._xaxis.series_u2p(this._xnudge);
var ystart = this._yaxis.series_u2p(this._yaxis.min);
var yend = this._yaxis.series_u2p(this._yaxis.max);
var bw = this.barWidth;
var bw2 = bw/2.0;
var points = [];
var yadj = this.offsetBars ? bw2 : 0;
for (var i=0, l=gridData.length; i<l; i++) {
if (this.data[i][0] == null) {
continue;
}
base = gridData[i][1];
// not stacked and first series in stack
if (this._plotData[i][1] < 0) {
if (this.varyBarColor && !this._stack) {
if (this.useNegativeColors) {
opts.fillStyle = negativeColors.next();
}
else {
opts.fillStyle = positiveColors.next();
}
}
}
else {
if (this.varyBarColor && !this._stack) {
opts.fillStyle = positiveColors.next();
}
else {
opts.fillStyle = positiveColor;
}
}
if (this.fill) {
if (this._plotData[i][1] >= 0) {
// xstart = this._xaxis.series_u2p(this._xnudge);
w = gridData[i][0] - xstart;
h = this.barWidth;
points = [xstart, base - bw2 - yadj, w, h];
}
else {
// xstart = this._xaxis.series_u2p(0);
w = xstart - gridData[i][0];
h = this.barWidth;
points = [gridData[i][0], base - bw2 - yadj, w, h];
}
this._barPoints.push([[points[0], points[1] + h], [points[0], points[1]], [points[0] + w, points[1]], [points[0] + w, points[1] + h]]);
if (shadow) {
this.renderer.shadowRenderer.draw(ctx, points);
}
var clr = opts.fillStyle || this.color;
this._dataColors.push(clr);
this.renderer.shapeRenderer.draw(ctx, points, opts);
}
else {
if (i === 0) {
points =[[xstart, ystart], [gridData[i][0], ystart], [gridData[i][0], gridData[i][1] - bw2 - yadj]];
}
else if (i < l-1) {
points = points.concat([[gridData[i-1][0], gridData[i-1][1] - bw2 - yadj], [gridData[i][0], gridData[i][1] + bw2 - yadj], [gridData[i][0], gridData[i][1] - bw2 - yadj]]);
}
// finally, draw the line
else {
points = points.concat([[gridData[i-1][0], gridData[i-1][1] - bw2 - yadj], [gridData[i][0], gridData[i][1] + bw2 - yadj], [gridData[i][0], yend], [xstart, yend]]);
if (shadow) {
this.renderer.shadowRenderer.draw(ctx, points);
}
var clr = opts.fillStyle || this.color;
this._dataColors.push(clr);
this.renderer.shapeRenderer.draw(ctx, points, opts);
}
}
}
}
if (this.highlightColors.length == 0) {
this.highlightColors = $.jqplot.computeHighlightColors(this._dataColors);
}
else if (typeof(this.highlightColors) == 'string') {
this.highlightColors = [];
for (var i=0; i<this._dataColors.length; i++) {
this.highlightColors.push(this.highlightColors);
}
}
};
// setup default renderers for axes and legend so user doesn't have to
// called with scope of plot
function preInit(target, data, options) {
options = options || {};
options.axesDefaults = options.axesDefaults || {};
options.grid = options.grid || {};
options.legend = options.legend || {};
options.seriesDefaults = options.seriesDefaults || {};
// only set these if there is a pie series
var setopts = false;
if (options.seriesDefaults.renderer === $.jqplot.PyramidRenderer) {
setopts = true;
}
else if (options.series) {
for (var i=0; i < options.series.length; i++) {
if (options.series[i].renderer === $.jqplot.PyramidRenderer) {
setopts = true;
}
}
}
if (setopts) {
options.axesDefaults.renderer = $.jqplot.PyramidAxisRenderer;
options.grid.renderer = $.jqplot.PyramidGridRenderer;
options.seriesDefaults.pointLabels = {show: false};
}
}
// called within context of plot
// create a canvas which we can draw on.
// insert it before the eventCanvas, so eventCanvas will still capture events.
function postPlotDraw() {
// Memory Leaks patch
if (this.plugins.pyramidRenderer && this.plugins.pyramidRenderer.highlightCanvas) {
this.plugins.pyramidRenderer.highlightCanvas.resetCanvas();
this.plugins.pyramidRenderer.highlightCanvas = null;
}
this.plugins.pyramidRenderer = {highlightedSeriesIndex:null};
this.plugins.pyramidRenderer.highlightCanvas = new $.jqplot.GenericCanvas();
this.eventCanvas._elem.before(this.plugins.pyramidRenderer.highlightCanvas.createElement(this._gridPadding, 'jqplot-pyramidRenderer-highlight-canvas', this._plotDimensions, this));
this.plugins.pyramidRenderer.highlightCanvas.setContext();
this.eventCanvas._elem.bind('mouseleave', {plot:this}, function (ev) { unhighlight(ev.data.plot); });
}
function highlight (plot, sidx, pidx, points) {
var s = plot.series[sidx];
var canvas = plot.plugins.pyramidRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0,canvas._ctx.canvas.width, canvas._ctx.canvas.height);
s._highlightedPoint = pidx;
plot.plugins.pyramidRenderer.highlightedSeriesIndex = sidx;
var opts = {fillStyle: s.highlightColors[pidx], fillRect: false};
s.renderer.shapeRenderer.draw(canvas._ctx, points, opts);
if (s.synchronizeHighlight !== false && plot.series.length >= s.synchronizeHighlight && s.synchronizeHighlight !== sidx) {
s = plot.series[s.synchronizeHighlight];
opts = {fillStyle: s.highlightColors[pidx], fillRect: false};
s.renderer.shapeRenderer.draw(canvas._ctx, s._barPoints[pidx], opts);
}
canvas = null;
}
function unhighlight (plot) {
var canvas = plot.plugins.pyramidRenderer.highlightCanvas;
canvas._ctx.clearRect(0,0, canvas._ctx.canvas.width, canvas._ctx.canvas.height);
for (var i=0; i<plot.series.length; i++) {
plot.series[i]._highlightedPoint = null;
}
plot.plugins.pyramidRenderer.highlightedSeriesIndex = null;
plot.target.trigger('jqplotDataUnhighlight');
canvas = null;
}
function handleMove(ev, gridpos, datapos, neighbor, plot) {
if (neighbor) {
var ins = [neighbor.seriesIndex, neighbor.pointIndex, neighbor.data];
var evt1 = jQuery.Event('jqplotDataMouseOver');
evt1.pageX = ev.pageX;
evt1.pageY = ev.pageY;
plot.target.trigger(evt1, ins);
if (plot.series[ins[0]].highlightMouseOver && !(ins[0] == plot.plugins.pyramidRenderer.highlightedSeriesIndex && ins[1] == plot.series[ins[0]]._highlightedPoint)) {
var evt = jQuery.Event('jqplotDataHighlight');
evt.which = ev.which;
evt.pageX = ev.pageX;
evt.pageY = ev.pageY;
plot.target.trigger(evt, ins);
highlight (plot, neighbor.seriesIndex, neighbor.pointIndex, neighbor.points);
}
}
else if (neighbor == null) {
unhighlight (plot);
}
}
// Have to add hook here, becuase it needs called before series is inited.
$.jqplot.preInitHooks.push(preInit);
})(jQuery);

View File

@@ -0,0 +1,223 @@
/**
* jqPlot
* Pure JavaScript plotting plugin using jQuery
*
* Version: 1.0.9
* Revision: d96a669
*
* Copyright (c) 2009-2016 Chris Leonello
* jqPlot is currently available for use in all personal or commercial projects
* under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
* version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
* choose the license that best suits your project and use it accordingly.
*
* Although not required, the author would appreciate an email letting him
* know of any substantial use of jqPlot. You can reach the author at:
* chris at jqplot dot com or see http://www.jqplot.com/info.php .
*
* If you are feeling kind and generous, consider supporting the project by
* making a donation at: http://www.jqplot.com/donate.php .
*
* sprintf functions contained in jqplot.sprintf.js by Ash Searle:
*
* version 2007.04.27
* author Ash Searle
* http://hexmen.com/blog/2007/03/printf-sprintf/
* http://hexmen.com/js/sprintf.js
* The author (Ash Searle) has placed this code in the public domain:
* "This code is unrestricted: you are free to use it however you like."
*
*/
(function($) {
/**
* Class: $.jqplot.Trendline
* Plugin which will automatically compute and draw trendlines for plotted data.
*/
$.jqplot.Trendline = function() {
// Group: Properties
// prop: show
// Wether or not to show the trend line.
this.show = $.jqplot.config.enablePlugins;
// prop: color
// CSS color spec for the trend line.
// By default this wil be the same color as the primary line.
this.color = '#666666';
// prop: renderer
// Renderer to use to draw the trend line.
// The data series that is plotted may not be rendered as a line.
// Therefore, we use our own line renderer here to draw a trend line.
this.renderer = new $.jqplot.LineRenderer();
// prop: rendererOptions
// Options to pass to the line renderer.
// By default, markers are not shown on trend lines.
this.rendererOptions = {marker:{show:false}};
// prop: label
// Label for the trend line to use in the legend.
this.label = '';
// prop: type
// Either 'exponential', 'exp', or 'linear'.
this.type = 'linear';
// prop: shadow
// true or false, whether or not to show the shadow.
this.shadow = true;
// prop: markerRenderer
// Renderer to use to draw markers on the line.
// I think this is wrong.
this.markerRenderer = {show:false};
// prop: lineWidth
// Width of the trend line.
this.lineWidth = 1.5;
// prop: shadowAngle
// Angle of the shadow on the trend line.
this.shadowAngle = 45;
// prop: shadowOffset
// pixel offset for each stroke of the shadow.
this.shadowOffset = 1.0;
// prop: shadowAlpha
// Alpha transparency of the shadow.
this.shadowAlpha = 0.07;
// prop: shadowDepth
// number of strokes to make of the shadow.
this.shadowDepth = 3;
this.isTrendline = true;
};
$.jqplot.postSeriesInitHooks.push(parseTrendLineOptions);
$.jqplot.postDrawSeriesHooks.push(drawTrendline);
$.jqplot.addLegendRowHooks.push(addTrendlineLegend);
// called witin scope of the legend object
// current series passed in
// must return null or an object {label:label, color:color}
function addTrendlineLegend(series) {
var ret = null;
if (series.trendline && series.trendline.show) {
var lt = series.trendline.label.toString();
if (lt) {
ret = {label:lt, color:series.trendline.color};
}
}
return ret;
}
// called within scope of a series
function parseTrendLineOptions (target, data, seriesDefaults, options, plot) {
if (this._type && (this._type === 'line' || this._type == 'bar')) {
this.trendline = new $.jqplot.Trendline();
options = options || {};
$.extend(true, this.trendline, {color:this.color}, seriesDefaults.trendline, options.trendline);
this.trendline.renderer.init.call(this.trendline, null);
}
}
// called within scope of series object
function drawTrendline(sctx, options) {
// if we have options, merge trendline options in with precedence
options = $.extend(true, {}, this.trendline, options);
if (this.trendline && options.show) {
var fit;
// this.renderer.setGridData.call(this);
var data = options.data || this.data;
fit = fitData(data, this.trendline.type);
var gridData = options.gridData || this.renderer.makeGridData.call(this, fit.data);
this.trendline.renderer.draw.call(this.trendline, sctx, gridData, {showLine:true, shadow:this.trendline.shadow});
}
}
function regression(x, y, typ) {
var type = (typ == null) ? 'linear' : typ;
var N = x.length;
var slope;
var intercept;
var SX = 0;
var SY = 0;
var SXX = 0;
var SXY = 0;
var SYY = 0;
var Y = [];
var X = [];
if (type == 'linear') {
X = x;
Y = y;
}
else if (type == 'exp' || type == 'exponential') {
for ( var i=0; i<y.length; i++) {
// ignore points <= 0, log undefined.
if (y[i] <= 0) {
N--;
}
else {
X.push(x[i]);
Y.push(Math.log(y[i]));
}
}
}
for ( var i = 0; i < N; i++) {
SX = SX + X[i];
SY = SY + Y[i];
SXY = SXY + X[i]* Y[i];
SXX = SXX + X[i]* X[i];
SYY = SYY + Y[i]* Y[i];
}
slope = (N*SXY - SX*SY)/(N*SXX - SX*SX);
intercept = (SY - slope*SX)/N;
return [slope, intercept];
}
function linearRegression(X,Y) {
var ret;
ret = regression(X,Y,'linear');
return [ret[0],ret[1]];
}
function expRegression(X,Y) {
var ret;
var x = X;
var y = Y;
ret = regression(x, y,'exp');
var base = Math.exp(ret[0]);
var coeff = Math.exp(ret[1]);
return [base, coeff];
}
function fitData(data, typ) {
var type = (typ == null) ? 'linear' : typ;
var ret;
var res;
var x = [];
var y = [];
var ypred = [];
for (i=0; i<data.length; i++){
if (data[i] != null && data[i][0] != null && data[i][1] != null) {
x.push(data[i][0]);
y.push(data[i][1]);
}
}
if (type == 'linear') {
ret = linearRegression(x,y);
for ( var i=0; i<x.length; i++){
res = ret[0]*x[i] + ret[1];
ypred.push([x[i], res]);
}
}
else if (type == 'exp' || type == 'exponential') {
ret = expRegression(x,y);
for ( var i=0; i<x.length; i++){
res = ret[1]*Math.pow(ret[0],x[i]);
ypred.push([x[i], res]);
}
}
return {data: ypred, slope: ret[0], intercept: ret[1]};
}
})(jQuery);

View File

@@ -10,11 +10,16 @@
</title>
<link rel="stylesheet" href="/js/jquery.mobile/jquery.mobile-1.4.5.min.css"/>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
{% block styles %}
{% endblock %}
<script type="text/javascript" src="/js/jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="/js/jquery.mobile/jquery.mobile-1.4.5.min.js"></script>
<script type="text/javascript" src="/js/jquery.timeago.js"></script>
<script type="text/javascript" src="/js/jquery-qrcode-0.17.0.min.js"></script>
<link rel="stylesheet" type="text/css" href="/css/style.css"/>
{% block scripts %}
{% endblock %}
<script type="text/javascript">
$.mobile.ajaxEnabled = false;
@@ -47,6 +52,7 @@
( '/inbox/new', 'new', 'mail', 'New' ),
( '/inbox/profile', 'profile', 'info', 'Profile' ),
( '/inbox/peers', 'peers', 'user', 'Peers' ),
( '/plugins', 'plugins', 'grid', 'Plugins' ),
] %}
{% set active_page = active_page|default('inbox') %}
@@ -54,7 +60,7 @@
<div data-role="navbar" data-iconpos="left">
<ul>
{% for href, id, icon, caption in navigation %}
<li>
<li class="navitem">
<a href="{{ href }}" id="{{ id }}" data-icon="{{ icon }}" class="{{ 'ui-btn-active' if active_page == id }}">{{ caption }}</a>
</li>
{% endfor %}

View File

@@ -26,6 +26,13 @@ window.onload = function() {
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
</li>
<li>
<form class="action" method="post" action="/reboot"
onsubmit="return confirm('this will reboot the unit, continue?');">
<input type="submit" class="button ui-btn ui-corner-all" value="Reboot"/>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>
</li>
<li>
<form class="action" method="post" action="/restart"
onsubmit="return confirm('This will restart the service in {{ other_mode }} mode, continue?');">

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% set active_page = "plugins" %}
{% block title %}
Plugins
{% endblock %}
{% block script %}
$(function(){
$('input[type=checkbox]').change(function(e) {
var checkbox = $(this);
var form = checkbox.closest('form');
var url = form.attr('action');
$.ajax({
type: 'POST',
url: url,
data: form.serialize(),
success: function(data) {
if( data.indexOf('failed') != -1 ) {
alert('Could not be toggled.');
}
}
});
});
});
{% endblock %}
{% block content %}
<div id="container">
{% for name in database.keys() %}
<div class="plugins-box">
<h4>
<a {% if name in loaded and loaded[name].on_webhook is defined %} href="/plugins/{{name}}" {% endif %}>{{name}}</a>
</h4>
<form method="POST" action="/plugins/toggle">
<input type="checkbox" data-role="flipswitch" name="enabled" id="flip-checkbox-{{name}}" data-on-text="Enabled" data-off-text="Disabled" data-wrapper-class="custom-size-flipswitch" {% if name in loaded %} checked {% endif %}>
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="plugin" value="{{ name }}"/>
</form>
</div>
{% endfor %}
</div>
{% endblock %}

View File

@@ -109,6 +109,9 @@ def load_config(args):
elif config['ui']['display']['type'] in ('ws_154inch', 'ws154inch', 'waveshare_154inch', 'waveshare154inch'):
config['ui']['display']['type'] = 'waveshare154inch'
elif config['ui']['display']['type'] in ('waveshare144lcd', 'ws_144inch', 'ws144inch', 'waveshare_144inch', 'waveshare144inch'):
config['ui']['display']['type'] = 'waveshare144lcd'
elif config['ui']['display']['type'] in ('ws_213d', 'ws213d', 'waveshare_213d', 'waveshare213d'):
config['ui']['display']['type'] = 'waveshare213d'

View File

@@ -54,4 +54,4 @@ ping -c 1 "${UNIT_HOSTNAME}" > /dev/null 2>&1 || {
}
echo "@ backing up $UNIT_HOSTNAME to $OUTPUT ..."
ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo find ${FILES_TO_BACKUP} -print0 | xargs -0 sudo tar cv" | gzip -9 > "$OUTPUT"
ssh "${UNIT_USERNAME}@${UNIT_HOSTNAME}" "sudo find ${FILES_TO_BACKUP} -type f -print0 | xargs -0 sudo tar cv" | gzip -9 > "$OUTPUT"

View File

@@ -45,12 +45,8 @@ def installer():
installer()
required = []
with open('requirements.txt') as fp:
for line in fp:
line = line.strip()
if line != "":
required.append(line)
required = [line.strip() for line in fp if line.strip() != ""]
import pwnagotchi