Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 93cc25e7ef | |||
| f3a22fa511 | |||
| 6981c821cc | |||
| 837906f6a5 | |||
| f0d1fe2301 | |||
| 28d0484db1 | |||
| 2d3b747d42 | |||
| c5d6c31d5f | |||
| 0a9b73417b | |||
| 62b5080d43 | |||
| 4e7f7344f2 | |||
| 5a3dccfa68 | |||
| c0a8e72e34 | |||
| fd2cf5b282 | |||
| 3523316c70 | |||
| bb9613ea41 | |||
| 79d107595c | |||
| ffdb162ad4 | |||
| 9d9a192d8b | |||
| 45711e18e7 | |||
| e57370588c | |||
| 5384cc82a1 | |||
| e770fdf78e | |||
| d318c3661a | |||
| b960473aed | |||
| c98b42a6cd | |||
| b7c2409655 | |||
| 3a98bf0907 | |||
| d42316d706 | |||
| de47450381 | |||
| 8bd17e6582 | |||
| bd08aa2641 | |||
| 5d79d509f8 | |||
| 7315381410 | |||
| 005382b3d4 | |||
| a68c72f99b | |||
| f5f9e53f48 | |||
| 27b508ba18 | |||
| 725c58f709 | |||
| e8ed8f780f | |||
| 008ff9df93 | |||
| 759fc6affe | |||
| ca88cd7d71 | |||
| ff796b17fc | |||
| 63ee66fb6d | |||
| c2b0722d3a | |||
| 50a1cb83db | |||
| e4522f11b7 | |||
| 8f8ef9753f | |||
| 537183078f | |||
| 766d9b09e6 | |||
| c049d5e8df | |||
| 47937d8bb8 | |||
| e21efc1b4a | |||
| 8edc170b75 | |||
| 4044c43292 | |||
| a19b17e19d | |||
| dbefb381df | |||
| 21bfae9fbc | |||
| 674692c2fc | |||
| 311137fd0a | |||
| 8d5503832a | |||
| be0da7e74d | |||
| 3ef2e80675 | |||
| 191feb7077 | |||
| 0a856e69c7 | |||
| 92364a86c2 | |||
| fd1f07d40d | |||
| 102b333ea0 | |||
| 6574a724b4 | |||
| bc0abbb985 | |||
| f4d95e2441 | |||
| efcba4cfb0 | |||
| 69e145aac6 | |||
| a6e44e6c56 | |||
| a6fa60a40a | |||
| 3c792cf286 | |||
| babd6d88dc | |||
| 21290682b6 | |||
| bd45b88477 | |||
| 6f5c01fccc | |||
| 6af5d94b8a | |||
| 2cd3ff1e78 | |||
| 2afc8c8708 | |||
| e8a6021e3f | |||
| da641cad7f | |||
| 75f3a6d1c0 | |||
| 06f1f9c10c | |||
| ef0fb8b990 | |||
| d81884eebd | |||
| 6dc3db1648 | |||
| fbca31d88a | |||
| 852e05d2c2 | |||
| ef90f0e4dc | |||
| 5dfbe6f951 | |||
| 54618be769 | |||
| 721c6ac0a7 | |||
| 7e3a4855d8 | |||
| 41ae988562 | |||
| 8f38cf62ac | |||
| 26890a8bbf | |||
| 879cb28b9e | |||
| 6c37a7b1a9 | |||
| 919dd41321 | |||
| 017721f659 | |||
| ee0d1184ed | |||
| b4352eb844 | |||
| dddd6e8632 | |||
| 548ae2be2c | |||
| 209f557a9b | |||
| 307972b9ec | |||
| 9bd22c5fff | |||
| 3f2e729d85 | |||
| c48da832e0 | |||
| 659fb74071 | |||
| 0035b2e6f3 | |||
| 0f58892d85 | |||
| cf8af255c6 | |||
| 5391985618 | |||
| a23ecd095c | |||
| ca819b84e7 | |||
| 4691b30bf3 | |||
| b37849ddef | |||
| 58c79ecbd4 | |||
| 888296e3e1 | |||
| 863b998eae | |||
| c78ed7323a | |||
| 9e69651f0c | |||
| 2211f7ca95 | |||
| dc12b92d5b | |||
| 559b1f2a7c | |||
| 7abc0ec624 | |||
| 91ef012688 | |||
| 0f5bd9fef4 | |||
| 45594aeac0 | |||
| c8b3ab2413 | |||
| 52464ec5a8 | |||
| 37db23a0e8 | |||
| c4fe5fbf37 | |||
| 8072fe9c36 | |||
| 2e263f1b14 | |||
| fe07f41425 | |||
| 4b7f96a378 | |||
| 02ebfd7ce2 | |||
| 2e53784002 | |||
| 4c35d0f179 | |||
| fc112d997b | |||
| 2e4c7e2683 | |||
| fa6675cb04 | |||
| 212ea9ec65 | |||
| c92fd63160 | |||
| f8b7306dd0 | |||
| b6f2379a38 | |||
| fc7a1bf167 | |||
| 3c55e96341 | |||
| 44a636010e | |||
| 36d77591f4 | |||
| c4fba54407 | |||
| 512f877039 |
@@ -0,0 +1 @@
|
|||||||
|
site/
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
Copyright (c) 2018, 2019, Kristjan Komloši, Jakob Kosec, Juš Dolžan,
|
||||||
|
TeraHz development team
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
||||||
|
SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
||||||
|
CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
# TeraHz
|
# TeraHz
|
||||||
|
|
||||||
|
[](https://terahz.readthedocs.io/en/latest/?badge=latest)
|
||||||
|
|
||||||
TeraHz is a low-cost spectrometer based on a Raspberry Pi 3 or 3 B+ and three sensors:
|
TeraHz is a low-cost spectrometer based on a Raspberry Pi 3 or 3 B+ and three sensors:
|
||||||
+ [__AS7265x__](https://www.tindie.com/products/onehorse/compact-as7265x-spectrometer/)
|
+ [__AS7265x__](https://www.tindie.com/products/onehorse/compact-as7265x-spectrometer/)
|
||||||
is a 18 channel spectrometer chipset that provides the device with spectral data
|
is a 18 channel spectrometer chipset that provides the device with spectral data
|
||||||
@@ -12,6 +15,9 @@ Because people and institutions could use an affordable and accurate light-analy
|
|||||||
## Development team
|
## Development team
|
||||||
Copyright 2018, 2019
|
Copyright 2018, 2019
|
||||||
|
|
||||||
- Kristjan "d3m1g0d" Komloši (electronics, middleware)
|
- Kristjan "cls-02" Komloši (electronics, sensor drivers, backend)
|
||||||
- Juš "ANormalPerson" Dolžan (backend)
|
- Jakob "D3m1j4ck" Kosec (frontend)
|
||||||
- Jakob "D3m1j4ck" Kosec (frontend, website)
|
|
||||||
|
|
||||||
|
I would also like to thank Juš "ANormalPerson" Dolžan, who decided to leave the
|
||||||
|
team, but helped me a lot with backend development.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# app.py - main backend program
|
||||||
|
'''Main TeraHz backend program'''
|
||||||
|
# All code in this file is licensed under the ISC license, provided in LICENSE.txt
|
||||||
|
from flask import Flask
|
||||||
|
import flask
|
||||||
|
import sensors
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
@app.route('/data')
|
||||||
|
def sendData():
|
||||||
|
'''Responder function for /data route'''
|
||||||
|
s = sensors.Spectrometer(path='/dev/serial0', baudrate=115200, tout=1)
|
||||||
|
u = sensors.UVSensor()
|
||||||
|
l = sensors.LxMeter()
|
||||||
|
response = flask.jsonify([s.getData(), l.getData(), u.getABI()])
|
||||||
|
response.headers.add('Access-Control-Allow-Origin', '*')
|
||||||
|
return response
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
$(document).ready(function() {
|
|
||||||
$(chart_id).highcharts({
|
|
||||||
chart: chart,
|
|
||||||
title: title,
|
|
||||||
xAxis: xAxis,
|
|
||||||
yAxis: yAxis,
|
|
||||||
series: series
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
from flask import Flask, redirect, url_for, request, render_template
|
|
||||||
app = Flask(__Name__)
|
|
||||||
URL = "" #Insert url of website here
|
|
||||||
|
|
||||||
@app.route('/list')
|
|
||||||
def list():
|
|
||||||
#Return list json
|
|
||||||
|
|
||||||
@app.route('/load'):
|
|
||||||
def load():
|
|
||||||
#Request args, load json
|
|
||||||
|
|
||||||
@app.route('/deposit'):
|
|
||||||
def deposit():
|
|
||||||
#Request .json, store json
|
|
||||||
if request.isJson():
|
|
||||||
content = request.get_json(url = URL)
|
|
||||||
return content
|
|
||||||
|
|
||||||
@app.route('/post', , methods = ['POST']):
|
|
||||||
def post():
|
|
||||||
request.post(url = URL, data = "") #Insert the data you wish to upload
|
|
||||||
|
|
||||||
@app.route('/graph')
|
|
||||||
def graph(chartID = 'chart_ID', chart_type = 'line', chart_height = 500):
|
|
||||||
chart = {"renderTo": chartID, "type": chart_type, "height": chart_height,}
|
|
||||||
series = [{"name": 'Label1', "data": [1,2,3]}, {"name": 'Label2', "data": [4, 5, 6]}]
|
|
||||||
title = {"text": 'My Title'}
|
|
||||||
xAxis = {"categories": ['xAxis Data1', 'xAxis Data2', 'xAxis Data3']}
|
|
||||||
yAxis = {"title": {"text": 'yAxis Label'}}
|
|
||||||
return render_template('index.html', chartID=chartID, chart=chart, series=series, title=title, xAxis=xAxis, yAxis=yAxis)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(debug = True, host='0.0.0.0', port=8080, passthrough_errors=True)
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# run.sh - run the backend server
|
||||||
|
sudo gunicorn app:app -b 0.0.0.0:5000 &
|
||||||
|
disown
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
# sensors.py - a module for interfacing to the sensors
|
# sensors.py - a module for interfacing to the sensors
|
||||||
|
'''Module for interfacing with TeraHz sensors'''
|
||||||
# Copyright 2019 Kristjan Komloši
|
# Copyright 2019 Kristjan Komloši
|
||||||
# The code in this file is licensed under the 3-clause BSD License
|
# All code in this file is licensed under the ISC license, provided in LICENSE.txt
|
||||||
|
|
||||||
import serial as ser
|
import serial as ser
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import smbus2
|
import smbus2
|
||||||
from sys import exit as ex
|
|
||||||
import time
|
|
||||||
|
|
||||||
class Spectrometer:
|
class Spectrometer:
|
||||||
|
'''Class representing the AS7265X specrometer'''
|
||||||
def initializeSensor(self):
|
def initializeSensor(self):
|
||||||
'''confirm the sensor is responding and proceed with spectrometer initialization'''
|
'''confirm the sensor is responding and proceed\
|
||||||
|
with spectrometer initialization'''
|
||||||
try:
|
try:
|
||||||
rstring = 'undefined' # just need it set to a value
|
rstring = 'undefined' # just need it set to a value
|
||||||
|
self.setParameters({'gain': 0})
|
||||||
self.serialObject.write(b'AT\n')
|
self.serialObject.write(b'AT\n')
|
||||||
rstring = self.serialObject.readline().decode()
|
rstring = self.serialObject.readline().decode()
|
||||||
if rstring == 'undefined':
|
if rstring == 'undefined':
|
||||||
@@ -22,8 +23,8 @@ class Spectrometer:
|
|||||||
if rstring == 'ERROR':
|
if rstring == 'ERROR':
|
||||||
raise Exception # sensor is in error state
|
raise Exception # sensor is in error state
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception ocurred when performing spectrometer handshake')
|
raise Exception(
|
||||||
ex(1)
|
'An exception ocurred when performing spectrometer handshake')
|
||||||
|
|
||||||
def setParameters(self, parameters):
|
def setParameters(self, parameters):
|
||||||
'''applies the parameters like LED light and gain to the spectrometer'''
|
'''applies the parameters like LED light and gain to the spectrometer'''
|
||||||
@@ -32,7 +33,8 @@ class Spectrometer:
|
|||||||
it_time = int(parameters['it_time'])
|
it_time = int(parameters['it_time'])
|
||||||
if it_time <= 0:
|
if it_time <= 0:
|
||||||
it_time = 1
|
it_time = 1
|
||||||
self.serialObject.write('ATINTTIME={}\n'.format(string(it_time)).encode())
|
self.serialObject.write(
|
||||||
|
'ATINTTIME={}\n'.format(str(it_time)).encode())
|
||||||
self.serialObject.readline()
|
self.serialObject.readline()
|
||||||
|
|
||||||
if 'gain' in parameters:
|
if 'gain' in parameters:
|
||||||
@@ -51,46 +53,51 @@ class Spectrometer:
|
|||||||
self.serialObject.write('ATLED3={}\n'.format(led).encode())
|
self.serialObject.write('ATLED3={}\n'.format(led).encode())
|
||||||
self.serialObject.readline()
|
self.serialObject.readline()
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured during spectrometer initialization')
|
raise Exception(
|
||||||
ex(1)
|
'An exception occured during spectrometer initialization')
|
||||||
|
|
||||||
def getData(self):
|
def getData(self):
|
||||||
|
'''Returns spectral data in a pandas DataFrame.'''
|
||||||
try:
|
try:
|
||||||
self.serialObject.write(b'ATCDATA\n')
|
self.serialObject.write(b'ATCDATA\n')
|
||||||
rawresp = self.serialObject.readline().decode()
|
rawresp = self.serialObject.readline().decode()
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occurred when polling for spectrometer data')
|
raise Exception(
|
||||||
ex(1)
|
'An exception occurred when polling for spectrometer data')
|
||||||
else:
|
else:
|
||||||
responseorder = [i for i in 'RSTUVWGHIJKLABCDEF']
|
responseorder = [i for i in 'RSTUVWGHIJKLABCDEF']
|
||||||
realorder = [i for i in 'ABCDEFGHRISJTUVWKL']
|
realorder = [i for i in 'ABCDEFGHRISJTUVWKL']
|
||||||
response = pd.Series([float(i)/35.0 for i in rawresp[:-3].split(',')], index=responseorder)
|
response = pd.Series(
|
||||||
return pd.DataFrame(response, index=realorder, columns = ['uW/cm^2'])
|
[float(i) / 35.0 for i in rawresp[:-3].split(',')], index=responseorder)
|
||||||
|
return pd.DataFrame(response, index=realorder, columns=['uW/cm^2']).to_dict()['uW/cm^2']
|
||||||
|
|
||||||
def __init__(self, path='/dev/ttyUSB0', baudrate=115200, tout=1):
|
def __init__(self, path='/dev/ttyUSB0', baudrate=115200, tout=1):
|
||||||
self.path = path
|
self.path = path
|
||||||
self.baudrate = baudrate
|
self.baudrate = baudrate
|
||||||
self.timeout = 1
|
self.timeout = 1
|
||||||
try:
|
try:
|
||||||
self.serialObject =
|
self.serialObject = ser.Serial(path, baudrate, timeout=tout)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when opening the serial port at {}'.format(path))
|
raise Exception(
|
||||||
ex(1)
|
'An exception occured when opening the serial port at {}'.format(path))
|
||||||
else:
|
else:
|
||||||
self.initializeSensor()
|
self.initializeSensor()
|
||||||
|
|
||||||
|
|
||||||
class LxMeter:
|
class LxMeter:
|
||||||
|
'''Class representing the APDS-9301 digital photometer.'''
|
||||||
def __init__(self, busNumber=1, addr=0x39):
|
def __init__(self, busNumber=1, addr=0x39):
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
try:
|
try:
|
||||||
self.bus = smbus2.SMBus(busNumber) # initialize the SMBus interface
|
# initialize the SMBus interface
|
||||||
|
self.bus = smbus2.SMBus(busNumber)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured opening the SMBus {}'.format(self.bus))
|
raise Exception(
|
||||||
|
'An exception occured opening the SMBus {}'.format(self.bus))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.bus.write_byte_data(self.addr, 0xa0, 0x03) # enable the sensor
|
self.bus.write_byte_data(
|
||||||
|
self.addr, 0xa0, 0x03) # enable the sensor
|
||||||
self.setGain(16)
|
self.setGain(16)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when enabling lux meter')
|
raise Exception('An exception occured when enabling lux meter')
|
||||||
@@ -102,13 +109,15 @@ class LxMeter:
|
|||||||
temp = self.bus.read_byte_data(self.addr, 0xa1)
|
temp = self.bus.read_byte_data(self.addr, 0xa1)
|
||||||
self.bus.write_byte_data(self.addr, 0xa1, 0xef & temp)
|
self.bus.write_byte_data(self.addr, 0xa1, 0xef & temp)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when setting lux meter gain')
|
raise Exception(
|
||||||
|
'An exception occured when setting lux meter gain')
|
||||||
if gain == 16:
|
if gain == 16:
|
||||||
try:
|
try:
|
||||||
temp = self.bus.read_byte_data(self.addr, 0xa1)
|
temp = self.bus.read_byte_data(self.addr, 0xa1)
|
||||||
self.bus.write_byte_data(self.addr, 0xa1, 0x10 | temp)
|
self.bus.write_byte_data(self.addr, 0xa1, 0x10 | temp)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when setting lux meter gain')
|
raise Exception(
|
||||||
|
'An exception occured when setting lux meter gain')
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid gain')
|
raise Exception('Invalid gain')
|
||||||
|
|
||||||
@@ -119,6 +128,8 @@ class LxMeter:
|
|||||||
return 16
|
return 16
|
||||||
if self.bus.read_byte_data(self.addr, 0xa1) & 0x10 == 0x00:
|
if self.bus.read_byte_data(self.addr, 0xa1) & 0x10 == 0x00:
|
||||||
return 1
|
return 1
|
||||||
|
raise Exception('An error occured when getting lux meter gain')
|
||||||
|
# Under normal conditions, this raise is unreachable.
|
||||||
except:
|
except:
|
||||||
raise Exception('An error occured when getting lux meter gain')
|
raise Exception('An error occured when getting lux meter gain')
|
||||||
|
|
||||||
@@ -130,14 +141,16 @@ class LxMeter:
|
|||||||
temp = self.bus.read_byte_data(self.addr, 0xa1)
|
temp = self.bus.read_byte_data(self.addr, 0xa1)
|
||||||
self.bus.write_byte_data(self.addr, 0xa1, (temp & 0xfc) | time)
|
self.bus.write_byte_data(self.addr, 0xa1, (temp & 0xfc) | time)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured setting lux integration time')
|
raise Exception(
|
||||||
|
'An exception occured setting lux integration time')
|
||||||
|
|
||||||
def getIntTime(self):
|
def getIntTime(self):
|
||||||
'''Get the lux sensor integration time.'''
|
'''Get the lux sensor integration time.'''
|
||||||
try:
|
try:
|
||||||
return self.bus.read_byte_data(self.addr, 0xa1) & 0x03
|
return self.bus.read_byte_data(self.addr, 0xa1) & 0x03
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured getting lux integration time')
|
raise Exception(
|
||||||
|
'An exception occured getting lux integration time')
|
||||||
|
|
||||||
def getData(self):
|
def getData(self):
|
||||||
'''return the calculated lux value'''
|
'''return the calculated lux value'''
|
||||||
@@ -160,18 +173,23 @@ class LxMeter:
|
|||||||
lux = 0
|
lux = 0
|
||||||
return lux
|
return lux
|
||||||
|
|
||||||
|
|
||||||
class UVSensor:
|
class UVSensor:
|
||||||
|
'''Class representing VEML6075 UVA/B meter'''
|
||||||
def __init__(self, bus=1, addr=0x10):
|
def __init__(self, bus=1, addr=0x10):
|
||||||
|
self.addr = addr
|
||||||
try:
|
try:
|
||||||
self.bus = smbus2.SMBus(bus)
|
self.bus = smbus2.SMBus(bus)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured opening SMBus {}'.format(bus))
|
raise Exception(
|
||||||
|
'An exception occured opening SMBus {}'.format(bus))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# enable the sensor and set the integration time
|
# enable the sensor and set the integration time
|
||||||
self.bus.write_byte_data(addr, 0x00, 0b00010000)
|
self.bus.write_byte_data(self.addr, 0x00, 0b00010000)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when initalizing the UV Sensor')
|
raise Exception(
|
||||||
|
'An exception occured when initalizing the UV Sensor')
|
||||||
|
|
||||||
def getABI(self):
|
def getABI(self):
|
||||||
'''Calculates the UVA and UVB irradiances,
|
'''Calculates the UVA and UVB irradiances,
|
||||||
@@ -179,35 +197,43 @@ class UVSensor:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# read the raw UVA, UVB and compensation values from the sensor
|
# read the raw UVA, UVB and compensation values from the sensor
|
||||||
aRaw = self.bus.read_word_data(addr, 0x07)
|
aRaw = self.bus.read_word_data(self.addr, 0x07)
|
||||||
bRaw = self.bus.read_word_data(addr, 0x09)
|
bRaw = self.bus.read_word_data(self.addr, 0x09)
|
||||||
c1 = self.bus.read_word_data(addr, 0x0a)
|
c1 = self.bus.read_word_data(self.addr, 0x0a)
|
||||||
c2 = self.bus.read_word_data(addr, 0x0b)
|
c2 = self.bus.read_word_data(self.addr, 0x0b)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when fetching raw UV data')
|
raise Exception('An exception occured when fetching raw UV data')
|
||||||
# scary computations ahead! refer to Vishay app note 84339 and Sparkfun
|
# scary computations ahead! refer to Vishay app note 84339 and Sparkfun
|
||||||
# VEML6075 documentation.
|
# VEML6075 documentation.
|
||||||
|
|
||||||
# first, compensate for visible and IR noise
|
# compensate for visible and IR noise
|
||||||
aCorr = aRaw - 2.22 * c1 - 1.33 * c2
|
aCorr = aRaw - 2.22 * c1 - 1.33 * c2
|
||||||
bCorr = bRaw - 2.95 * c1 - 1.74 * c2
|
bCorr = bRaw - 2.95 * c1 - 1.74 * c2
|
||||||
|
|
||||||
# second, convert values into irradiances
|
# convert values into irradiances
|
||||||
a = aCorr * 0.00110
|
a = aCorr * 1.06
|
||||||
b = bCorr * 0.00125
|
b = bCorr * 0.48
|
||||||
|
|
||||||
|
# zero out negative results (readings with no uv)
|
||||||
|
if a < 0:
|
||||||
|
a = 0
|
||||||
|
if b < 0:
|
||||||
|
b = 0
|
||||||
# last, calculate the UV index
|
# last, calculate the UV index
|
||||||
i = (uva + uvb) / 2
|
i = (a + b) / 2
|
||||||
|
|
||||||
return [a, b, i]
|
return [a, b, i]
|
||||||
|
|
||||||
def getA(self):
|
def getA(self):
|
||||||
|
'''Returns UVA value. A getABI() wrapper.'''
|
||||||
return self.getABI()[0]
|
return self.getABI()[0]
|
||||||
|
|
||||||
def getB(self):
|
def getB(self):
|
||||||
|
'''Returns UVB value. A getABI() wrapper.'''
|
||||||
return self.getABI()[1]
|
return self.getABI()[1]
|
||||||
|
|
||||||
def getI(self):
|
def getI(self):
|
||||||
|
'''Returns UV index. A getABI() wrapper.'''
|
||||||
return self.getABI()[2]
|
return self.getABI()[2]
|
||||||
|
|
||||||
def on(self):
|
def on(self):
|
||||||
@@ -215,15 +241,17 @@ class UVSensor:
|
|||||||
try:
|
try:
|
||||||
# write the default value for power on
|
# write the default value for power on
|
||||||
# no configurable params = no bitmask
|
# no configurable params = no bitmask
|
||||||
self.bus.write_byte_data(addr, 0x00, 0x10)
|
self.bus.write_byte_data(self.addr, 0x00, 0x10)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when turning the UV sensor on')
|
raise Exception(
|
||||||
|
'An exception occured when turning the UV sensor on')
|
||||||
|
|
||||||
def off(self):
|
def off(self):
|
||||||
'''Shuts the UV sensor down.'''
|
'''Shuts the UV sensor down.'''
|
||||||
try:
|
try:
|
||||||
# write the default value + the shutdown bit
|
# write the default value + the shutdown bit
|
||||||
# no configurable params = no bitmask
|
# no configurable params = no bitmask
|
||||||
self.bus.write_byte_data(addr, 0x00, 0x11)
|
self.bus.write_byte_data(self.addr, 0x00, 0x11)
|
||||||
except:
|
except:
|
||||||
raise Exception('An exception occured when shutting the UV sensor down')
|
raise Exception(
|
||||||
|
'An exception occured when shutting the UV sensor down')
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
# storage.py - storage backend for TeraHz
|
# storage.py - storage backend for TeraHz
|
||||||
|
'''TeraHz storage backend'''
|
||||||
# Copyright Kristjan Komloši 2019
|
# Copyright Kristjan Komloši 2019
|
||||||
# This code is licensed under the 3-clause BSD license
|
# All code in this file is licensed under the ISC license,
|
||||||
|
# provided in LICENSE.txt
|
||||||
|
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
class jsonStorage:
|
class jsonStorage:
|
||||||
|
'''Class for simple sqlite3 database of JSON entries'''
|
||||||
def __init__(self, dbFile):
|
def __init__(self, dbFile):
|
||||||
'''Storage object constructor. Argument is filename'''
|
'''Storage object constructor. Argument is filename'''
|
||||||
self.db = sqlite3.connect(dbFile)
|
self.db = sqlite3.connect(dbFile)
|
||||||
|
|
||||||
def listJSONs(self):
|
def listJSONs(self):
|
||||||
|
'''Returns a list of all existing entries.'''
|
||||||
c = self.db.cursor()
|
c = self.db.cursor()
|
||||||
c.execute('SELECT * FROM storage')
|
c.execute('SELECT * FROM storage')
|
||||||
result = c.fetchall()
|
result = c.fetchall()
|
||||||
@@ -17,8 +21,10 @@ class jsonStorage:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def storeJSON(self, jsonString, comment):
|
def storeJSON(self, jsonString, comment):
|
||||||
|
'''Stores a JSON entry along with a timestamp and a comment.'''
|
||||||
c = self.db.cursor()
|
c = self.db.cursor()
|
||||||
c.execute('INSERT INTO storage VALUES (datetime(\'now\', \'localtime\'), ?, ?)', (comment, jsonString))
|
c.execute(('INSERT INTO storage VALUES (datetime'
|
||||||
|
'(\'now\', \'localtime\'), ?, ?)'), (comment, jsonString))
|
||||||
c.close()
|
c.close()
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
# Minimal flup configuration for Flask
|
||||||
|
from flup.server.fcgi import WSGIServer
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
WSGIServer(app, bindAddress='/var/www/api/terahz.sock').run()
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
# TeraHz build guide
|
||||||
|
This document describes the process of building/installing TeraHz from the Git
|
||||||
|
repository.
|
||||||
|
|
||||||
|
## Warning
|
||||||
|
The recommended way of getting TeraHz is the official Raspberry Pi SD card image
|
||||||
|
provided under the releases tab in the GitHub repository. Installing TeraHz from
|
||||||
|
source is a time consuming and painful process, even more so if you don't know
|
||||||
|
what you're doing, and whatever you end up building __will not be officially
|
||||||
|
supported__ (unless you're a core developer).
|
||||||
|
|
||||||
|
With this warning out of the way, let's begin.
|
||||||
|
|
||||||
|
## Getting the latest sources
|
||||||
|
The most reliable way to get working source code is by cloning the official
|
||||||
|
GitHub repository and checking out the `development-stable` tag. This tag marks
|
||||||
|
the latest confirmed working commit. Building from the master branch is somewhat
|
||||||
|
risky, and building from development branches is straight up stupid if you're
|
||||||
|
not a developer.
|
||||||
|
|
||||||
|
Make sure that the repository is cloned into `/home/pi/TeraHz`, as Lighttpd
|
||||||
|
expects to find frontend files inside this directory.
|
||||||
|
|
||||||
|
After cloning and checking out, check the documentation for module dependencies
|
||||||
|
and the required version of python in the `docs/dependencies.md` file.
|
||||||
|
|
||||||
|
## Installing Python
|
||||||
|
This step depends a lot on the platform you're using. TeraHz was developed with
|
||||||
|
Raspberry Pi and Raspbian in mind. If you're familiar with Raspbian enough,
|
||||||
|
you'll know that the latest version of Python available is `3.5`, which is too
|
||||||
|
obsolete to run TeraHz and the required modules consistently. This leaves us
|
||||||
|
with compiling Python from source. __This step is guaranteed to be slow,
|
||||||
|
overnight compiling with something like tmux is recommended.__
|
||||||
|
|
||||||
|
### Pre-requirements
|
||||||
|
Installing python without most C libraries will lead to a rather minimalistic
|
||||||
|
Python install, missing a lot of important modules. To prevent this, update
|
||||||
|
the system packages. After that, reboot.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt update
|
||||||
|
sudo apt full-upgrade
|
||||||
|
sudo reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the required build tools, libraries and their headers.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt-get install build-essential tk-dev libncurses5-dev libncursesw5-dev \
|
||||||
|
libreadline6-dev libdb5.3-dev libgdbm-dev libsqlite3-dev libssl-dev libbz2-dev \
|
||||||
|
libexpat1-dev liblzma-dev zlib1g-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiling
|
||||||
|
Compiling Python from source is, in fact pretty easy, just time-consuming. To combat
|
||||||
|
that, using tmux to detach and later reattach the session is advised.
|
||||||
|
|
||||||
|
Python is packaged in many forms, but you'll be using the most basic
|
||||||
|
of them all: a gzipped tarball. Download and decompress it, then cd into its
|
||||||
|
directory.
|
||||||
|
|
||||||
|
```
|
||||||
|
wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz
|
||||||
|
tar -xzf Python-3.6.8.tgz
|
||||||
|
cd Python-3.6.8
|
||||||
|
```
|
||||||
|
|
||||||
|
Python's build process is pretty classic, a `.configure` script and a Makefile.
|
||||||
|
Using the `-j` option with Make can reduce the compile time significantly. Go
|
||||||
|
with as many threads as you have cores: `-j 4` works great on the Pi 3 B/B+.
|
||||||
|
|
||||||
|
```
|
||||||
|
./configure
|
||||||
|
make -j4
|
||||||
|
```
|
||||||
|
|
||||||
|
When the compilation ends, install your freshly built version of python.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo make altinstall
|
||||||
|
```
|
||||||
|
|
||||||
|
"Altinstall" means that the new version of Python will be installed beside the
|
||||||
|
existing version, and all related commands will use the full naming scheme:
|
||||||
|
think `python3.6` or `pip3.6` instead of the shorter `python3` or `pip3`.
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
Another painfully slow part is the installation of all the required modules
|
||||||
|
needed by TeraHz. Luckily, `pip3.6` takes care of the entire installation
|
||||||
|
process. As before, using tmux is advised.
|
||||||
|
|
||||||
|
```
|
||||||
|
pip3.6 install smbus pyserial flask pandas
|
||||||
|
```
|
||||||
|
|
||||||
|
## Raspi-config
|
||||||
|
For some law-obeying reason, Raspbian locks down the Wi-Fi interface card until
|
||||||
|
the Wi-Fi country is set. This means that we will need to set it manually through
|
||||||
|
the `raspi-config` program. It requires superuser privileges.
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo raspi-config
|
||||||
|
```
|
||||||
|

|
||||||
|
|
||||||
|
Configure the network hostname to something specific. If setting up multiple
|
||||||
|
TeraHz machines, make their hostnames unique so you can tell them apart.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Enable SSH and I2C interfaces.
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Expand the root filesystem along the entire SD card.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Set the Wi-Fi country to the country you'll be using TeraHz in.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Save and reboot to enable Wi-Fi
|
||||||
|
|
||||||
|
## Installing packages
|
||||||
|
In addition to what's already installed, TeraHz requires the following daemons
|
||||||
|
to run:
|
||||||
|
- Lighttpd - Frontend HTTP server
|
||||||
|
- Dnsmasq - DNS and DHCP server, used to redirect the `terahz.site` domain
|
||||||
|
- Hostapd - Wi-Fi access point
|
||||||
|
|
||||||
|
They are available from the Raspbian repository. Install it via `apt`.
|
||||||
|
|
||||||
|
```
|
||||||
|
apt install hostapd dnsmasq hostapd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuring daemons
|
||||||
|
By default, the daemons we installed are disabled and start only manually. To
|
||||||
|
change that, enable them through systemctl. Hostapd conflicts with
|
||||||
|
wpa_supplicant, the solution is to disable wpa_supplicant (this will break your
|
||||||
|
wireless connections, so use wired ethernet).
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo systemctl unmask hostapd
|
||||||
|
sudo systemctl stop wpa_supplicant
|
||||||
|
sudo systemctl disable wpa_supplicant
|
||||||
|
sudo systemctl enable dnsmasq hostapd lighttpd
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copying configuration files
|
||||||
|
To simplify the process of configuring Raspbian to run TeraHz, sample
|
||||||
|
configuration file are provided in the `etcs` subdirectory of the Git
|
||||||
|
repository.
|
||||||
|
|
||||||
|
These files have been verified to work, but it's not a brilliant idea to just
|
||||||
|
copy them into your `/etc` directory. Use them carefully and more as a template
|
||||||
|
for your own configuration rather than as a _de facto_ way of configuring
|
||||||
|
TeraHz.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Development-stable dependencies
|
||||||
|
The current development version of TeraHz has been verified to work with:
|
||||||
|
|
||||||
|
- Raspbian Stretch (9)
|
||||||
|
- Python 3.6.8 (built from source code and altinstall'd)
|
||||||
|
- Module versions (direct `pip3.6 list` output):
|
||||||
|
|
||||||
|
```
|
||||||
|
Package Version
|
||||||
|
--------------- ---------
|
||||||
|
Click 7.0
|
||||||
|
Flask 1.0.3
|
||||||
|
itsdangerous 1.1.0
|
||||||
|
Jinja2 2.10.1
|
||||||
|
MarkupSafe 1.1.1
|
||||||
|
numpy 1.16.4
|
||||||
|
pandas 0.24.2
|
||||||
|
pip 18.1
|
||||||
|
pyserial 3.4
|
||||||
|
python-dateutil 2.8.0
|
||||||
|
pytz 2019.1
|
||||||
|
setuptools 40.6.2
|
||||||
|
six 1.12.0
|
||||||
|
smbus 1.1.post2
|
||||||
|
Werkzeug 0.15.4
|
||||||
|
```
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# TeraHz developer's guide
|
||||||
|
This document explains how TeraHz works. It's a good starting point for developers
|
||||||
|
and an interesting read for the curious.
|
||||||
|
|
||||||
|
# Hardware
|
||||||
|
TeraHz was developed on and for the Raspberry Pi 3 Model B+. Compatibility with
|
||||||
|
other Raspberries can probably be achieved by tweaking the device paths in the
|
||||||
|
`app.py` file, but isn't confirmed at this point. Theoretically, 3 Model B and
|
||||||
|
Zero W should work out of the box, but models without Wi-Fi will need an
|
||||||
|
external Wi-Fi adapter if Wi-Fi functionality is desired. The practicality of
|
||||||
|
compiling Python on the first generation of Raspberry Pis is also very
|
||||||
|
questionable.
|
||||||
|
|
||||||
|
Sensors required for operation are:
|
||||||
|
+ AS7265x
|
||||||
|
+ VEML6075
|
||||||
|
+ APDS-9301
|
||||||
|
|
||||||
|
They provide the spectrometry data, UV data and illuminance data, respectively.
|
||||||
|
They all support I2C, AS7265x supports UART in addition.
|
||||||
|
|
||||||
|
The sensors leech power from the GPIO connector, thus eliminating the need for a
|
||||||
|
separate power supply. The necessary power for the whole system is delivered through
|
||||||
|
the Raspberry's USB port. This also allows for considerable versatility, as it
|
||||||
|
enables the resulting device to be either wall-powered or battery-powered.
|
||||||
|
In a portable configuration, I used a one-cell power bank, which allowed for
|
||||||
|
about 45 minutes of continuous operation.
|
||||||
|
|
||||||
|
## AS7265x chipset
|
||||||
|
_[Datasheet][1ds] [Buy breakout board][1]_
|
||||||
|
|
||||||
|
This chipset supports either I2C or UART. Because transferring large amounts of
|
||||||
|
data over I2C is rather cumbersome, TeraHz uses AS7265x in UART mode.
|
||||||
|
|
||||||
|
This chipset consists of three rather small surface-mounted chips and requires
|
||||||
|
an EEPROM. To lower the complexity of assembly for the end-user, I recommend
|
||||||
|
using a breakout board.
|
||||||
|
|
||||||
|
The serial UART connection operates at 115200 baud, which seems to be the
|
||||||
|
standard for most recent embedded peripherals. As with most serial hardware,
|
||||||
|
the TxD and RxD lines must be crossed over when connecting to the processor.
|
||||||
|
|
||||||
|
Communication with the sensor is simple and clear through AT commands. There's
|
||||||
|
a lot of them, all documented inside the datasheet, but the most important one
|
||||||
|
is `ATGETCDATA`, which returns the calibrated spectral data from the sensors.
|
||||||
|
|
||||||
|
The data is returned in the form of a comma-separated list of floating point
|
||||||
|
values, ending with a newline. The order is alphabetical, which is __different
|
||||||
|
from wavelength order__. See the datasheet for more information.
|
||||||
|
|
||||||
|
## VEML6075
|
||||||
|
_[Datasheet][2ds] [Buy breakout board][2]_
|
||||||
|
|
||||||
|
This chip communicates through I2C and provides TeraHz with UVA and UVB
|
||||||
|
irradiance readings. It's not an ideal chip for this task, as it's been marked
|
||||||
|
End-of-Life by Vishay and it'll have to be replaced with a better one in future
|
||||||
|
hardware versions of TeraHz.
|
||||||
|
|
||||||
|
The chip resides at the I2C address `0x10`. There's not a lot of communication
|
||||||
|
required: at initialization, the integration time has to be set and after that,
|
||||||
|
the sensor is ready to go.
|
||||||
|
|
||||||
|
16-bit UV values lie in two two-byte registers, `0x07` for UVA and `0x09` for
|
||||||
|
UVB. For correct result conversion, there are also two correction registers,
|
||||||
|
UVCOMP_1 and 2, located at `0x0A` and `0x0B`, respectively.
|
||||||
|
|
||||||
|
To convert these four values into irradiances, they must be multiplied by
|
||||||
|
certain constants, somewhat loosely defined in the sensor datasheet. Keep in
|
||||||
|
mind that the way of computing the "irradiance" is very much experimentally
|
||||||
|
derived, and even Vishay's tech support doesn't know how exactly to calculate
|
||||||
|
the irradiance.
|
||||||
|
|
||||||
|
## APDS-9301
|
||||||
|
_[Datasheet][3ds] [Buy breakout board][3]_
|
||||||
|
This chip measures illuminance in luxes and like the VEML6075, connects through
|
||||||
|
I2C. Unlike the VEML6075, this chip is very good at its job, providing accurate
|
||||||
|
and fast results without undefined mathematics or required calibration.
|
||||||
|
|
||||||
|
At power-on, it needs to be enabled and the sensor gain set to the high setting,
|
||||||
|
as the formula for Lux calculation is only defined for that setting. This
|
||||||
|
initialization is handled by the sensors module.
|
||||||
|
|
||||||
|
The lux reading is derived from two channels, descriptively called CH0 and CH1,
|
||||||
|
residing in respective 16-bit registers at addresses `0xAC` and `0xAE`. After a
|
||||||
|
successful read of both data registers, the lux value can be derived using the
|
||||||
|
formula in the sensor's datasheet.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[1]: https://www.tindie.com/products/onehorse/compact-as7265x-spectrometer/
|
||||||
|
[2]: https://www.sparkfun.com/products/15089
|
||||||
|
[3]: https://www.sparkfun.com/products/14350
|
||||||
|
[1ds]: sensor-docs/AS7265x.pdf
|
||||||
|
[2ds]: sensor-docs/veml6075.pdf
|
||||||
|
[3ds]: sensor-docs/APDS-9301.pdf
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# TeraHz Electrical Guide
|
||||||
|
This section briefly explains the neccessary electrical connections between the
|
||||||
|
Raspberry Pi and the sensors you'll need to make to ensure correct and safe
|
||||||
|
operation.
|
||||||
|
|
||||||
|
As mentioned before, TeraHz requires 3 sensors to operate. The simpler UVA/UVB
|
||||||
|
sensor and the ambient light analyzer connect to the Raspberry's SMBus (I2C)
|
||||||
|
bus, while the spectrometer connects via high-speed UART.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## PCBs vs breakout boards & jumpers
|
||||||
|
The Raspberry Pi GPIO port includes enough power pins to require only jumper
|
||||||
|
cables to connect the sensors to the Raspberry Pi. However, this is not a great
|
||||||
|
idea. During development, jumper cables have repeatedly been proven to be an
|
||||||
|
unreliable nuisance, and their absolute lack of rigidity helped me fry one of my
|
||||||
|
development Raspberry Pis. For this reason, I wholeheartedly recommend using a
|
||||||
|
simple PCB to route the connections from the Pi to the sensors. At this time,
|
||||||
|
there is no official TeraHz PCB, but it shall be announced and included in the
|
||||||
|
project when basic testing will be done.
|
||||||
|
|
||||||
|
GPIO can be routed to the PCB with a standard old IDE disk cable, and terminated
|
||||||
|
with another 40-pin connector at the PCB. Sensor breakouts should be mounted
|
||||||
|
<<<<<<< HEAD
|
||||||
|
through standard 0.1" connectors, male on the sensor breakout and female on the
|
||||||
|
PCB. A shitty add-on header and a shitty add-on header v1.69bis can't hurt, either.
|
||||||
|
=======
|
||||||
|
through standard 0.1" connectors, male on the sensor brakout and female on the
|
||||||
|
PCB. A shitty addon header and a shitty addon header v1.69bis can't hurt, either.
|
||||||
|
>>>>>>> fd1f07d40dace3e003e49377d4771de53f8bdeb8
|
||||||
|
|
||||||
|
## SMBus sensors
|
||||||
|
SMBus is a well-defined version of the well-known I2C bus, widely used
|
||||||
|
in computer motherboards for low-band bandwidth communication with various ICs,
|
||||||
|
especially sensors and power-supply related devices. This bus is broken out on
|
||||||
|
the Raspberry Pi GPIO port as the "I2C1" bus (see picture).
|
||||||
|
|
||||||
|
Pins are familiarly marked as SDA and SCL, the same as with classic I2C. They
|
||||||
|
connect to the SDA and SCL pins on the VEML6075 and APDS-9301 sensor.
|
||||||
|
|
||||||
|
## UART sensor
|
||||||
|
<<<<<<< HEAD
|
||||||
|
Spectral sensor attaches through the UART port on the Raspberry pi (see picture).
|
||||||
|
=======
|
||||||
|
Spectrometry sensor attaches through the UART port on the Raspberry pi (see picture).
|
||||||
|
>>>>>>> fd1f07d40dace3e003e49377d4771de53f8bdeb8
|
||||||
|
|
||||||
|
The Tx and Rx lines must cross over, connecting the sensor's Tx line to the
|
||||||
|
computer's Rx line and vice versa.
|
||||||
|
|
||||||
|
## Power supply
|
||||||
|
As the sensors require only a small amount of power, they can be powered directly from the Raspberry Pi itself, leeching power from the 3.3V lines.
|
||||||
|
|
||||||
|
## Ground
|
||||||
|
There's not a lot to say here, connect sensor GND to Pi's GND.
|
||||||
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 63 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<img alt="TeraHz logo" src="imgs/logo-sq.png" width="200px">
|
||||||
|
# TeraHz documentation - index
|
||||||
|
This is the starting point of TeraHz documentation.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
interface=wlan0
|
||||||
|
dhcp-range=192.168.1.10,192.168.1.100,255.255.255.0,24h
|
||||||
|
address=/terahz.site/192.168.1.1
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# edit_ssid.sh - edits hostapd.conf and sets a MAC address-based SSID
|
||||||
|
ssid=`ip link | awk '/wlan0/ {getline; print $2}' | awk -v FS=':' '{printf("TeraHz_%s%s%s\n", $4, $5, $6)}'`
|
||||||
|
sed "/ssid=.*/s/ssid=.*/ssid=$ssid/" hostapd.conf > newconf
|
||||||
|
mv newconf hostapd.conf
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
interface=wlan0
|
||||||
|
hw_mode=g
|
||||||
|
channel=8
|
||||||
|
wpa=2
|
||||||
|
wpa_key_mgmt=WPA-PSK
|
||||||
|
wpa_pairwise=TKIP
|
||||||
|
rsn_pairwise=CCMP
|
||||||
|
ssid=TeraHz
|
||||||
|
wpa_passphrase=terahertz
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# install.sh - install TeraHz onto a Raspbian or DietPi installation
|
||||||
|
apt -y update
|
||||||
|
apt -y full-upgrade
|
||||||
|
apt install -y python3 python3-pip lighttpd dnsmasq hostapd libatlas-base-dev
|
||||||
|
pip3 install numpy pandas flask smbus2 pyserial
|
||||||
|
|
||||||
|
cp -R hostapd/ /etc
|
||||||
|
cp -R lighttpd/ /etc
|
||||||
|
cp dnsmasq.conf /etc
|
||||||
|
cp rc.local /etc
|
||||||
|
cp -R ../frontend/* /var/www/html
|
||||||
|
mkdir -p /usr/local/lib/terahz
|
||||||
|
cp -R ../backend/* /usr/local/lib/terahz
|
||||||
|
cd /etc/hostapd
|
||||||
|
chmod +x edit_ssid.sh
|
||||||
|
./edit_ssid.sh
|
||||||
|
|
||||||
|
systemctl unmask dnsmasq hostapd lighttpd
|
||||||
|
systemctl enable dnsmasq hostapd lighttpd
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
server.modules = (
|
||||||
|
"mod_access",
|
||||||
|
"mod_alias",
|
||||||
|
"mod_compress",
|
||||||
|
"mod_redirect"
|
||||||
|
)
|
||||||
|
|
||||||
|
server.document-root = "/var/www/html"
|
||||||
|
server.upload-dirs = ( "/var/cache/lighttpd/uploads" )
|
||||||
|
server.errorlog = "/var/log/lighttpd/error.log"
|
||||||
|
server.pid-file = "/var/run/lighttpd.pid"
|
||||||
|
server.username = "www-data"
|
||||||
|
server.groupname = "www-data"
|
||||||
|
server.port = 80
|
||||||
|
|
||||||
|
|
||||||
|
index-file.names = ( "index.php", "index.html", "index.lighttpd.html" )
|
||||||
|
url.access-deny = ( "~", ".inc" )
|
||||||
|
static-file.exclude-extensions = ( ".php", ".pl", ".fcgi" )
|
||||||
|
|
||||||
|
compress.cache-dir = "/var/cache/lighttpd/compress/"
|
||||||
|
compress.filetype = ( "application/javascript", "text/css", "text/html", "text/plain" )
|
||||||
|
|
||||||
|
# default listening port for IPv6 falls back to the IPv4 port
|
||||||
|
include_shell "/usr/share/lighttpd/use-ipv6.pl " + server.port
|
||||||
|
include_shell "/usr/share/lighttpd/create-mime.assign.pl"
|
||||||
|
include_shell "/usr/share/lighttpd/include-conf-enabled.pl"
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
#
|
||||||
|
# rc.local
|
||||||
|
#
|
||||||
|
# This script is executed at the end of each multiuser runlevel.
|
||||||
|
# Make sure that the script will "exit 0" on success or any other
|
||||||
|
# value on error.
|
||||||
|
#
|
||||||
|
# In order to enable or disable this script just change the execution
|
||||||
|
# bits.
|
||||||
|
#
|
||||||
|
# By default this script does nothing.
|
||||||
|
/usr/local/lib/terahz/run.sh &
|
||||||
|
disown
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
@app.route('/<txt>')
|
|
||||||
def root(txt):
|
|
||||||
return 'txt={}'.format(txt)
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from flask import Flask
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
@app.route('/', methods = ['GET', 'POST'])
|
|
||||||
def index():
|
|
||||||
value = request.json['key']
|
|
||||||
return value
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
app.run
|
|
||||||
|
|
||||||
|
|
||||||
test = open("JsonJs", "r")
|
|
||||||
req = requests.post('C:/Users/Janez%20Dolzan/Documents/Python%20projects/Spektrometer/GitHub/TeraHz/frontend/website.html')
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
var output = document.getElementById('output');
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// All code in this file is licensed under the ISC license, provided in LICENSE.txt
|
||||||
|
$('#update').click(function () {
|
||||||
|
updateData();
|
||||||
|
});
|
||||||
|
// jQuery event binder
|
||||||
|
|
||||||
|
function updateData () {
|
||||||
|
const url = 'http://' + window.location.hostname + ':5000/data';
|
||||||
|
$.ajax({ // spawn an AJAX request
|
||||||
|
url: url,
|
||||||
|
success: function (data, status) {
|
||||||
|
console.log(data);
|
||||||
|
graphSpectralData(data[0], 0);
|
||||||
|
fillTableData(data);
|
||||||
|
},
|
||||||
|
timeout: 2500 // this should be a pretty sane timeout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function graphSpectralData (obj, dom) {
|
||||||
|
// graph spectral data in obj into dom
|
||||||
|
var graphPoints = [];
|
||||||
|
var graphXTicks = [];
|
||||||
|
|
||||||
|
Object.keys(obj).forEach((element, index) => {
|
||||||
|
graphPoints.push([index, obj[element]]); // build array of points
|
||||||
|
graphXTicks.push([index, element]); // build array of axis labels
|
||||||
|
});
|
||||||
|
// console.log(graphPoints);
|
||||||
|
const options = {
|
||||||
|
grid: {color: 'white'},
|
||||||
|
xaxis: {ticks: graphXTicks}
|
||||||
|
};
|
||||||
|
$.plot('#graph', [graphPoints], options);
|
||||||
|
// flot expects an array of arrays (lines) of 2-element arrays (points)
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillTableData (obj) {
|
||||||
|
// fill the obj data into HTML tables
|
||||||
|
Object.keys(obj[0])
|
||||||
|
.forEach((element) => { $('#' + element).text(obj[0][element]); });
|
||||||
|
$('#lx').text(obj[1]);
|
||||||
|
$('#uva').text(obj[2][0]);
|
||||||
|
$('#uvb').text(obj[2][1]);
|
||||||
|
$('#uvi').text(obj[2][2]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<link rel="stylesheet" href="lib/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="stylesheet.css">
|
||||||
|
<title>TeraHz</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container text-center">
|
||||||
|
<h1><img src="lib/logo-sq.png" height="64px">TeraHz</h1>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="container">
|
||||||
|
<button id="update" class="btn btn-primary m-1 float-right">Get data</button>
|
||||||
|
<p id="debug">
|
||||||
|
</p>
|
||||||
|
<h3>Spectrogram</h3>
|
||||||
|
<div id="graph" style="height:480px;width:720px"></div>
|
||||||
|
<h3>Spectral readings</h3>
|
||||||
|
<table class="table table-dark table-sm" id="specter">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Band</th>
|
||||||
|
<th>Wavelength [nm]</th>
|
||||||
|
<th>Irradiance [μW/cm²]</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr>
|
||||||
|
<td>A</td>
|
||||||
|
<td>410 nm</td>
|
||||||
|
<td id="A">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>B</td>
|
||||||
|
<td>435 nm</td>
|
||||||
|
<td id="B">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>C</td>
|
||||||
|
<td>460 nm</td>
|
||||||
|
<td id="C">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>D</td>
|
||||||
|
<td>485 nm</td>
|
||||||
|
<td id="D">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>E</td>
|
||||||
|
<td>510 nm</td>
|
||||||
|
<td id="E">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>F</td>
|
||||||
|
<td>535 nm</td>
|
||||||
|
<td id="F">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>G</td>
|
||||||
|
<td>560 nm</td>
|
||||||
|
<td id="G">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>H</td>
|
||||||
|
<td>585 nm</td>
|
||||||
|
<td id="H">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>R</td>
|
||||||
|
<td>610 nm</td>
|
||||||
|
<td id="R">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>I</td>
|
||||||
|
<td>645 nm</td>
|
||||||
|
<td id="I">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>S</td>
|
||||||
|
<td>680 nm</td>
|
||||||
|
<td id="S">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>J</td>
|
||||||
|
<td>705 nm</td>
|
||||||
|
<td id="J">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>T</td>
|
||||||
|
<td>730 nm</td>
|
||||||
|
<td id="T">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>U</td>
|
||||||
|
<td>760 nm</td>
|
||||||
|
<td id="U">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-secondary">
|
||||||
|
<td>V</td>
|
||||||
|
<td>810 nm</td>
|
||||||
|
<td id="V">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-secondary">
|
||||||
|
<td>W</td>
|
||||||
|
<td>860 nm</td>
|
||||||
|
<td id="W">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-secondary">
|
||||||
|
<td>K</td>
|
||||||
|
<td>900 nm</td>
|
||||||
|
<td id="K">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="table-secondary">
|
||||||
|
<td>L</td>
|
||||||
|
<td>940 nm</td>
|
||||||
|
<td id="L">---</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<br>
|
||||||
|
<h3>Lux and UV readings</h3>
|
||||||
|
<table class="table-dark table" id="luxuv">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Parameter</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tr>
|
||||||
|
<td>Illuminance [lx]</td>
|
||||||
|
<td id="lx">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>UVA irradiance [μW/cm²]</td>
|
||||||
|
<td id="uva">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>UVB irradiance [μW/cm²]</td>
|
||||||
|
<td id="uvb">---</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>UVA/UVB average [μW/cm²]</td>
|
||||||
|
<td id="uvi">---</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<script src="lib/bootstrap.bundle.min.js"></script>
|
||||||
|
<script src="lib/jquery-3.4.1.min.js"></script>
|
||||||
|
<script src="lib/flot/jquery.flot.js"></script>
|
||||||
|
<script src="frontend.js"></script>
|
||||||
|
<script src="lib/flot/jquery.canvaswrapper.js"></script>
|
||||||
|
<script src="lib/flot/jquery.colorhelpers.js"></script>
|
||||||
|
<script src="lib/flot/jquery.flot.js"></script>
|
||||||
|
<script src="lib/flot/jquery.flot.saturated.js"></script>
|
||||||
|
<script src="lib/flot/jquery.flot.browser.js"></script>
|
||||||
|
<script src="lib/flot/jquery.flot.drawSeries.js"></script>
|
||||||
|
<script src="lib/flot/jquery.flot.uiConstants.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,550 @@
|
|||||||
|
/** ## jquery.flot.canvaswrapper
|
||||||
|
|
||||||
|
This plugin contains the function for creating and manipulating both the canvas
|
||||||
|
layers and svg layers.
|
||||||
|
|
||||||
|
The Canvas object is a wrapper around an HTML5 canvas tag.
|
||||||
|
The constructor Canvas(cls, container) takes as parameters cls,
|
||||||
|
the list of classes to apply to the canvas adnd the containter,
|
||||||
|
element onto which to append the canvas. The canvas operations
|
||||||
|
don't work unless the canvas is attached to the DOM.
|
||||||
|
|
||||||
|
### jquery.canvaswrapper.js API functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
var Canvas = function(cls, container) {
|
||||||
|
var element = container.getElementsByClassName(cls)[0];
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
element = document.createElement('canvas');
|
||||||
|
element.className = cls;
|
||||||
|
element.style.direction = 'ltr';
|
||||||
|
element.style.position = 'absolute';
|
||||||
|
element.style.left = '0px';
|
||||||
|
element.style.top = '0px';
|
||||||
|
|
||||||
|
container.appendChild(element);
|
||||||
|
|
||||||
|
// If HTML5 Canvas isn't available, throw
|
||||||
|
|
||||||
|
if (!element.getContext) {
|
||||||
|
throw new Error('Canvas is not available.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.element = element;
|
||||||
|
|
||||||
|
var context = this.context = element.getContext('2d');
|
||||||
|
this.pixelRatio = $.plot.browser.getPixelRatio(context);
|
||||||
|
|
||||||
|
// Size the canvas to match the internal dimensions of its container
|
||||||
|
var width = $(container).width();
|
||||||
|
var height = $(container).height();
|
||||||
|
this.resize(width, height);
|
||||||
|
|
||||||
|
// Collection of HTML div layers for text overlaid onto the canvas
|
||||||
|
|
||||||
|
this.SVGContainer = null;
|
||||||
|
this.SVG = {};
|
||||||
|
|
||||||
|
// Cache of text fragments and metrics, so we can avoid expensively
|
||||||
|
// re-calculating them when the plot is re-rendered in a loop.
|
||||||
|
|
||||||
|
this._textCache = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- resize(width, height)
|
||||||
|
|
||||||
|
Resizes the canvas to the given dimensions.
|
||||||
|
The width represents the new width of the canvas, meanwhile the height
|
||||||
|
is the new height of the canvas, both of them in pixels.
|
||||||
|
*/
|
||||||
|
|
||||||
|
Canvas.prototype.resize = function(width, height) {
|
||||||
|
var minSize = 10;
|
||||||
|
width = width < minSize ? minSize : width;
|
||||||
|
height = height < minSize ? minSize : height;
|
||||||
|
|
||||||
|
var element = this.element,
|
||||||
|
context = this.context,
|
||||||
|
pixelRatio = this.pixelRatio;
|
||||||
|
|
||||||
|
// Resize the canvas, increasing its density based on the display's
|
||||||
|
// pixel ratio; basically giving it more pixels without increasing the
|
||||||
|
// size of its element, to take advantage of the fact that retina
|
||||||
|
// displays have that many more pixels in the same advertised space.
|
||||||
|
|
||||||
|
// Resizing should reset the state (excanvas seems to be buggy though)
|
||||||
|
|
||||||
|
if (this.width !== width) {
|
||||||
|
element.width = width * pixelRatio;
|
||||||
|
element.style.width = width + 'px';
|
||||||
|
this.width = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.height !== height) {
|
||||||
|
element.height = height * pixelRatio;
|
||||||
|
element.style.height = height + 'px';
|
||||||
|
this.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the context, so we can reset in case we get replotted. The
|
||||||
|
// restore ensure that we're really back at the initial state, and
|
||||||
|
// should be safe even if we haven't saved the initial state yet.
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
context.save();
|
||||||
|
|
||||||
|
// Scale the coordinate space to match the display density; so even though we
|
||||||
|
// may have twice as many pixels, we still want lines and other drawing to
|
||||||
|
// appear at the same size; the extra pixels will just make them crisper.
|
||||||
|
|
||||||
|
context.scale(pixelRatio, pixelRatio);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
- clear()
|
||||||
|
|
||||||
|
Clears the entire canvas area, not including any overlaid HTML text
|
||||||
|
*/
|
||||||
|
Canvas.prototype.clear = function() {
|
||||||
|
this.context.clearRect(0, 0, this.width, this.height);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
- render()
|
||||||
|
|
||||||
|
Finishes rendering the canvas, including managing the text overlay.
|
||||||
|
*/
|
||||||
|
Canvas.prototype.render = function() {
|
||||||
|
var cache = this._textCache;
|
||||||
|
|
||||||
|
// For each text layer, add elements marked as active that haven't
|
||||||
|
// already been rendered, and remove those that are no longer active.
|
||||||
|
|
||||||
|
for (var layerKey in cache) {
|
||||||
|
if (hasOwnProperty.call(cache, layerKey)) {
|
||||||
|
var layer = this.getSVGLayer(layerKey),
|
||||||
|
layerCache = cache[layerKey];
|
||||||
|
|
||||||
|
var display = layer.style.display;
|
||||||
|
layer.style.display = 'none';
|
||||||
|
|
||||||
|
for (var styleKey in layerCache) {
|
||||||
|
if (hasOwnProperty.call(layerCache, styleKey)) {
|
||||||
|
var styleCache = layerCache[styleKey];
|
||||||
|
for (var key in styleCache) {
|
||||||
|
if (hasOwnProperty.call(styleCache, key)) {
|
||||||
|
var val = styleCache[key],
|
||||||
|
positions = val.positions;
|
||||||
|
|
||||||
|
for (var i = 0, position; positions[i]; i++) {
|
||||||
|
position = positions[i];
|
||||||
|
if (position.active) {
|
||||||
|
if (!position.rendered) {
|
||||||
|
layer.appendChild(position.element);
|
||||||
|
position.rendered = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
positions.splice(i--, 1);
|
||||||
|
if (position.rendered) {
|
||||||
|
while (position.element.firstChild) {
|
||||||
|
position.element.removeChild(position.element.firstChild);
|
||||||
|
}
|
||||||
|
position.element.parentNode.removeChild(position.element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positions.length === 0) {
|
||||||
|
if (val.measured) {
|
||||||
|
val.measured = false;
|
||||||
|
} else {
|
||||||
|
delete styleCache[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.style.display = display;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
- getSVGLayer(classes)
|
||||||
|
|
||||||
|
Creates (if necessary) and returns the SVG overlay container.
|
||||||
|
The classes string represents the string of space-separated CSS classes
|
||||||
|
used to uniquely identify the text layer. It return the svg-layer div.
|
||||||
|
*/
|
||||||
|
Canvas.prototype.getSVGLayer = function(classes) {
|
||||||
|
var layer = this.SVG[classes];
|
||||||
|
|
||||||
|
// Create the SVG layer if it doesn't exist
|
||||||
|
|
||||||
|
if (!layer) {
|
||||||
|
// Create the svg layer container, if it doesn't exist
|
||||||
|
|
||||||
|
var svgElement;
|
||||||
|
|
||||||
|
if (!this.SVGContainer) {
|
||||||
|
this.SVGContainer = document.createElement('div');
|
||||||
|
this.SVGContainer.className = 'flot-svg';
|
||||||
|
this.SVGContainer.style.position = 'absolute';
|
||||||
|
this.SVGContainer.style.top = '0px';
|
||||||
|
this.SVGContainer.style.left = '0px';
|
||||||
|
this.SVGContainer.style.height = '100%';
|
||||||
|
this.SVGContainer.style.width = '100%';
|
||||||
|
this.SVGContainer.style.pointerEvents = 'none';
|
||||||
|
this.element.parentNode.appendChild(this.SVGContainer);
|
||||||
|
|
||||||
|
svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||||
|
svgElement.style.width = '100%';
|
||||||
|
svgElement.style.height = '100%';
|
||||||
|
|
||||||
|
this.SVGContainer.appendChild(svgElement);
|
||||||
|
} else {
|
||||||
|
svgElement = this.SVGContainer.firstChild;
|
||||||
|
}
|
||||||
|
|
||||||
|
layer = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
||||||
|
layer.setAttribute('class', classes);
|
||||||
|
layer.style.position = 'absolute';
|
||||||
|
layer.style.top = '0px';
|
||||||
|
layer.style.left = '0px';
|
||||||
|
layer.style.bottom = '0px';
|
||||||
|
layer.style.right = '0px';
|
||||||
|
svgElement.appendChild(layer);
|
||||||
|
this.SVG[classes] = layer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
- getTextInfo(layer, text, font, angle, width)
|
||||||
|
|
||||||
|
Creates (if necessary) and returns a text info object.
|
||||||
|
The object looks like this:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
width //Width of the text's wrapper div.
|
||||||
|
height //Height of the text's wrapper div.
|
||||||
|
element //The HTML div containing the text.
|
||||||
|
positions //Array of positions at which this text is drawn.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The positions array contains objects that look like this:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
active //Flag indicating whether the text should be visible.
|
||||||
|
rendered //Flag indicating whether the text is currently visible.
|
||||||
|
element //The HTML div containing the text.
|
||||||
|
text //The actual text and is identical with element[0].textContent.
|
||||||
|
x //X coordinate at which to draw the text.
|
||||||
|
y //Y coordinate at which to draw the text.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Each position after the first receives a clone of the original element.
|
||||||
|
The idea is that that the width, height, and general 'identity' of the
|
||||||
|
text is constant no matter where it is placed; the placements are a
|
||||||
|
secondary property.
|
||||||
|
|
||||||
|
Canvas maintains a cache of recently-used text info objects; getTextInfo
|
||||||
|
either returns the cached element or creates a new entry.
|
||||||
|
|
||||||
|
The layer parameter is string of space-separated CSS classes uniquely
|
||||||
|
identifying the layer containing this text.
|
||||||
|
Text is the text string to retrieve info for.
|
||||||
|
Font is either a string of space-separated CSS classes or a font-spec object,
|
||||||
|
defining the text's font and style.
|
||||||
|
Angle is the angle at which to rotate the text, in degrees. Angle is currently unused,
|
||||||
|
it will be implemented in the future.
|
||||||
|
The last parameter is the Maximum width of the text before it wraps.
|
||||||
|
The method returns a text info object.
|
||||||
|
*/
|
||||||
|
Canvas.prototype.getTextInfo = function(layer, text, font, angle, width) {
|
||||||
|
var textStyle, layerCache, styleCache, info;
|
||||||
|
|
||||||
|
// Cast the value to a string, in case we were given a number or such
|
||||||
|
|
||||||
|
text = '' + text;
|
||||||
|
|
||||||
|
// If the font is a font-spec object, generate a CSS font definition
|
||||||
|
|
||||||
|
if (typeof font === 'object') {
|
||||||
|
textStyle = font.style + ' ' + font.variant + ' ' + font.weight + ' ' + font.size + 'px/' + font.lineHeight + 'px ' + font.family;
|
||||||
|
} else {
|
||||||
|
textStyle = font;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve (or create) the cache for the text's layer and styles
|
||||||
|
|
||||||
|
layerCache = this._textCache[layer];
|
||||||
|
|
||||||
|
if (layerCache == null) {
|
||||||
|
layerCache = this._textCache[layer] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
styleCache = layerCache[textStyle];
|
||||||
|
|
||||||
|
if (styleCache == null) {
|
||||||
|
styleCache = layerCache[textStyle] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = generateKey(text);
|
||||||
|
info = styleCache[key];
|
||||||
|
|
||||||
|
// If we can't find a matching element in our cache, create a new one
|
||||||
|
|
||||||
|
if (!info) {
|
||||||
|
var element = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
||||||
|
if (text.indexOf('<br>') !== -1) {
|
||||||
|
addTspanElements(text, element, -9999);
|
||||||
|
} else {
|
||||||
|
var textNode = document.createTextNode(text);
|
||||||
|
element.appendChild(textNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
element.style.position = 'absolute';
|
||||||
|
element.style.maxWidth = width;
|
||||||
|
element.setAttributeNS(null, 'x', -9999);
|
||||||
|
element.setAttributeNS(null, 'y', -9999);
|
||||||
|
|
||||||
|
if (typeof font === 'object') {
|
||||||
|
element.style.font = textStyle;
|
||||||
|
element.style.fill = font.fill;
|
||||||
|
} else if (typeof font === 'string') {
|
||||||
|
element.setAttribute('class', font);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.getSVGLayer(layer).appendChild(element);
|
||||||
|
var elementRect = element.getBBox();
|
||||||
|
|
||||||
|
info = styleCache[key] = {
|
||||||
|
width: elementRect.width,
|
||||||
|
height: elementRect.height,
|
||||||
|
measured: true,
|
||||||
|
element: element,
|
||||||
|
positions: []
|
||||||
|
};
|
||||||
|
|
||||||
|
//remove elements from dom
|
||||||
|
while (element.firstChild) {
|
||||||
|
element.removeChild(element.firstChild);
|
||||||
|
}
|
||||||
|
element.parentNode.removeChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
info.measured = true;
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateTransforms (element, transforms) {
|
||||||
|
element.transform.baseVal.clear();
|
||||||
|
if (transforms) {
|
||||||
|
transforms.forEach(function(t) {
|
||||||
|
element.transform.baseVal.appendItem(t);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- addText (layer, x, y, text, font, angle, width, halign, valign, transforms)
|
||||||
|
|
||||||
|
Adds a text string to the canvas text overlay.
|
||||||
|
The text isn't drawn immediately; it is marked as rendering, which will
|
||||||
|
result in its addition to the canvas on the next render pass.
|
||||||
|
|
||||||
|
The layer is string of space-separated CSS classes uniquely
|
||||||
|
identifying the layer containing this text.
|
||||||
|
X and Y represents the X and Y coordinate at which to draw the text.
|
||||||
|
and text is the string to draw
|
||||||
|
*/
|
||||||
|
Canvas.prototype.addText = function(layer, x, y, text, font, angle, width, halign, valign, transforms) {
|
||||||
|
var info = this.getTextInfo(layer, text, font, angle, width),
|
||||||
|
positions = info.positions;
|
||||||
|
|
||||||
|
// Tweak the div's position to match the text's alignment
|
||||||
|
|
||||||
|
if (halign === 'center') {
|
||||||
|
x -= info.width / 2;
|
||||||
|
} else if (halign === 'right') {
|
||||||
|
x -= info.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valign === 'middle') {
|
||||||
|
y -= info.height / 2;
|
||||||
|
} else if (valign === 'bottom') {
|
||||||
|
y -= info.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
y += 0.75 * info.height;
|
||||||
|
|
||||||
|
|
||||||
|
// Determine whether this text already exists at this position.
|
||||||
|
// If so, mark it for inclusion in the next render pass.
|
||||||
|
|
||||||
|
for (var i = 0, position; positions[i]; i++) {
|
||||||
|
position = positions[i];
|
||||||
|
if (position.x === x && position.y === y && position.text === text) {
|
||||||
|
position.active = true;
|
||||||
|
// update the transforms
|
||||||
|
updateTransforms(position.element, transforms);
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (position.active === false) {
|
||||||
|
position.active = true;
|
||||||
|
position.text = text;
|
||||||
|
if (text.indexOf('<br>') !== -1) {
|
||||||
|
y -= 0.25 * info.height;
|
||||||
|
addTspanElements(text, position.element, x);
|
||||||
|
} else {
|
||||||
|
position.element.textContent = text;
|
||||||
|
}
|
||||||
|
position.element.setAttributeNS(null, 'x', x);
|
||||||
|
position.element.setAttributeNS(null, 'y', y);
|
||||||
|
position.x = x;
|
||||||
|
position.y = y;
|
||||||
|
// update the transforms
|
||||||
|
updateTransforms(position.element, transforms);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the text doesn't exist at this position, create a new entry
|
||||||
|
|
||||||
|
// For the very first position we'll re-use the original element,
|
||||||
|
// while for subsequent ones we'll clone it.
|
||||||
|
|
||||||
|
position = {
|
||||||
|
active: true,
|
||||||
|
rendered: false,
|
||||||
|
element: positions.length ? info.element.cloneNode() : info.element,
|
||||||
|
text: text,
|
||||||
|
x: x,
|
||||||
|
y: y
|
||||||
|
};
|
||||||
|
|
||||||
|
positions.push(position);
|
||||||
|
|
||||||
|
if (text.indexOf('<br>') !== -1) {
|
||||||
|
y -= 0.25 * info.height;
|
||||||
|
addTspanElements(text, position.element, x);
|
||||||
|
} else {
|
||||||
|
position.element.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move the element to its final position within the container
|
||||||
|
position.element.setAttributeNS(null, 'x', x);
|
||||||
|
position.element.setAttributeNS(null, 'y', y);
|
||||||
|
position.element.style.textAlign = halign;
|
||||||
|
// update the transforms
|
||||||
|
updateTransforms(position.element, transforms);
|
||||||
|
};
|
||||||
|
|
||||||
|
var addTspanElements = function(text, element, x) {
|
||||||
|
var lines = text.split('<br>'),
|
||||||
|
tspan, i, offset;
|
||||||
|
|
||||||
|
for (i = 0; i < lines.length; i++) {
|
||||||
|
if (!element.childNodes[i]) {
|
||||||
|
tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
|
||||||
|
element.appendChild(tspan);
|
||||||
|
} else {
|
||||||
|
tspan = element.childNodes[i];
|
||||||
|
}
|
||||||
|
tspan.textContent = lines[i];
|
||||||
|
offset = i * 1 + 'em';
|
||||||
|
tspan.setAttributeNS(null, 'dy', offset);
|
||||||
|
tspan.setAttributeNS(null, 'x', x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- removeText (layer, x, y, text, font, angle)
|
||||||
|
|
||||||
|
The function removes one or more text strings from the canvas text overlay.
|
||||||
|
If no parameters are given, all text within the layer is removed.
|
||||||
|
|
||||||
|
Note that the text is not immediately removed; it is simply marked as
|
||||||
|
inactive, which will result in its removal on the next render pass.
|
||||||
|
This avoids the performance penalty for 'clear and redraw' behavior,
|
||||||
|
where we potentially get rid of all text on a layer, but will likely
|
||||||
|
add back most or all of it later, as when redrawing axes, for example.
|
||||||
|
|
||||||
|
The layer is a string of space-separated CSS classes uniquely
|
||||||
|
identifying the layer containing this text. The following parameter are
|
||||||
|
X and Y coordinate of the text.
|
||||||
|
Text is the string to remove, while the font is either a string of space-separated CSS
|
||||||
|
classes or a font-spec object, defining the text's font and style.
|
||||||
|
*/
|
||||||
|
Canvas.prototype.removeText = function(layer, x, y, text, font, angle) {
|
||||||
|
var info, htmlYCoord;
|
||||||
|
if (text == null) {
|
||||||
|
var layerCache = this._textCache[layer];
|
||||||
|
if (layerCache != null) {
|
||||||
|
for (var styleKey in layerCache) {
|
||||||
|
if (hasOwnProperty.call(layerCache, styleKey)) {
|
||||||
|
var styleCache = layerCache[styleKey];
|
||||||
|
for (var key in styleCache) {
|
||||||
|
if (hasOwnProperty.call(styleCache, key)) {
|
||||||
|
var positions = styleCache[key].positions;
|
||||||
|
positions.forEach(function(position) {
|
||||||
|
position.active = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
info = this.getTextInfo(layer, text, font, angle);
|
||||||
|
positions = info.positions;
|
||||||
|
positions.forEach(function(position) {
|
||||||
|
htmlYCoord = y + 0.75 * info.height;
|
||||||
|
if (position.x === x && position.y === htmlYCoord && position.text === text) {
|
||||||
|
position.active = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
- clearCache()
|
||||||
|
|
||||||
|
Clears the cache used to speed up the text size measurements.
|
||||||
|
As an (unfortunate) side effect all text within the text Layer is removed.
|
||||||
|
Use this function before plot.setupGrid() and plot.draw() if the plot just
|
||||||
|
became visible or the styles changed.
|
||||||
|
*/
|
||||||
|
Canvas.prototype.clearCache = function() {
|
||||||
|
var cache = this._textCache;
|
||||||
|
for (var layerKey in cache) {
|
||||||
|
if (hasOwnProperty.call(cache, layerKey)) {
|
||||||
|
var layer = this.getSVGLayer(layerKey);
|
||||||
|
while (layer.firstChild) {
|
||||||
|
layer.removeChild(layer.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this._textCache = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
function generateKey(text) {
|
||||||
|
return text.replace(/0|1|2|3|4|5|6|7|8|9/g, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.Flot) {
|
||||||
|
window.Flot = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
window.Flot.Canvas = Canvas;
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
/* Plugin for jQuery for working with colors.
|
||||||
|
*
|
||||||
|
* Version 1.1.
|
||||||
|
*
|
||||||
|
* Inspiration from jQuery color animation plugin by John Resig.
|
||||||
|
*
|
||||||
|
* Released under the MIT license by Ole Laursen, October 2009.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
*
|
||||||
|
* $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
|
||||||
|
* var c = $.color.extract($("#mydiv"), 'background-color');
|
||||||
|
* console.log(c.r, c.g, c.b, c.a);
|
||||||
|
* $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
|
||||||
|
*
|
||||||
|
* Note that .scale() and .add() return the same modified object
|
||||||
|
* instead of making a new one.
|
||||||
|
*
|
||||||
|
* V. 1.1: Fix error handling so e.g. parsing an empty string does
|
||||||
|
* produce a color rather than just crashing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
$.color = {};
|
||||||
|
|
||||||
|
// construct color object with some convenient chainable helpers
|
||||||
|
$.color.make = function (r, g, b, a) {
|
||||||
|
var o = {};
|
||||||
|
o.r = r || 0;
|
||||||
|
o.g = g || 0;
|
||||||
|
o.b = b || 0;
|
||||||
|
o.a = a != null ? a : 1;
|
||||||
|
|
||||||
|
o.add = function (c, d) {
|
||||||
|
for (var i = 0; i < c.length; ++i) {
|
||||||
|
o[c.charAt(i)] += d;
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.normalize();
|
||||||
|
};
|
||||||
|
|
||||||
|
o.scale = function (c, f) {
|
||||||
|
for (var i = 0; i < c.length; ++i) {
|
||||||
|
o[c.charAt(i)] *= f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return o.normalize();
|
||||||
|
};
|
||||||
|
|
||||||
|
o.toString = function () {
|
||||||
|
if (o.a >= 1.0) {
|
||||||
|
return "rgb(" + [o.r, o.g, o.b].join(",") + ")";
|
||||||
|
} else {
|
||||||
|
return "rgba(" + [o.r, o.g, o.b, o.a].join(",") + ")";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
o.normalize = function () {
|
||||||
|
function clamp(min, value, max) {
|
||||||
|
return value < min ? min : (value > max ? max : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
o.r = clamp(0, parseInt(o.r), 255);
|
||||||
|
o.g = clamp(0, parseInt(o.g), 255);
|
||||||
|
o.b = clamp(0, parseInt(o.b), 255);
|
||||||
|
o.a = clamp(0, o.a, 1);
|
||||||
|
return o;
|
||||||
|
};
|
||||||
|
|
||||||
|
o.clone = function () {
|
||||||
|
return $.color.make(o.r, o.b, o.g, o.a);
|
||||||
|
};
|
||||||
|
|
||||||
|
return o.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract CSS color property from element, going up in the DOM
|
||||||
|
// if it's "transparent"
|
||||||
|
$.color.extract = function (elem, css) {
|
||||||
|
var c;
|
||||||
|
|
||||||
|
do {
|
||||||
|
c = elem.css(css).toLowerCase();
|
||||||
|
// keep going until we find an element that has color, or
|
||||||
|
// we hit the body or root (have no parent)
|
||||||
|
if (c !== '' && c !== 'transparent') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
elem = elem.parent();
|
||||||
|
} while (elem.length && !$.nodeName(elem.get(0), "body"));
|
||||||
|
|
||||||
|
// catch Safari's way of signalling transparent
|
||||||
|
if (c === "rgba(0, 0, 0, 0)") {
|
||||||
|
c = "transparent";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $.color.parse(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse CSS color string (like "rgb(10, 32, 43)" or "#fff"),
|
||||||
|
// returns color object, if parsing failed, you get black (0, 0,
|
||||||
|
// 0) out
|
||||||
|
$.color.parse = function (str) {
|
||||||
|
var res, m = $.color.make;
|
||||||
|
|
||||||
|
// Look for rgb(num,num,num)
|
||||||
|
res = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str);
|
||||||
|
if (res) {
|
||||||
|
return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for rgba(num,num,num,num)
|
||||||
|
res = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)
|
||||||
|
if (res) {
|
||||||
|
return m(parseInt(res[1], 10), parseInt(res[2], 10), parseInt(res[3], 10), parseFloat(res[4]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for rgb(num%,num%,num%)
|
||||||
|
res = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*\)/.exec(str);
|
||||||
|
if (res) {
|
||||||
|
return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for rgba(num%,num%,num%,num)
|
||||||
|
res = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str);
|
||||||
|
if (res) {
|
||||||
|
return m(parseFloat(res[1]) * 2.55, parseFloat(res[2]) * 2.55, parseFloat(res[3]) * 2.55, parseFloat(res[4]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for #a0b1c2
|
||||||
|
res = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str);
|
||||||
|
if (res) {
|
||||||
|
return m(parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for #fff
|
||||||
|
res = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str);
|
||||||
|
if (res) {
|
||||||
|
return m(parseInt(res[1] + res[1], 16), parseInt(res[2] + res[2], 16), parseInt(res[3] + res[3], 16));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, we're most likely dealing with a named color
|
||||||
|
var name = $.trim(str).toLowerCase();
|
||||||
|
if (name === "transparent") {
|
||||||
|
return m(255, 255, 255, 0);
|
||||||
|
} else {
|
||||||
|
// default to black
|
||||||
|
res = lookupColors[name] || [0, 0, 0];
|
||||||
|
return m(res[0], res[1], res[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lookupColors = {
|
||||||
|
aqua: [0, 255, 255],
|
||||||
|
azure: [240, 255, 255],
|
||||||
|
beige: [245, 245, 220],
|
||||||
|
black: [0, 0, 0],
|
||||||
|
blue: [0, 0, 255],
|
||||||
|
brown: [165, 42, 42],
|
||||||
|
cyan: [0, 255, 255],
|
||||||
|
darkblue: [0, 0, 139],
|
||||||
|
darkcyan: [0, 139, 139],
|
||||||
|
darkgrey: [169, 169, 169],
|
||||||
|
darkgreen: [0, 100, 0],
|
||||||
|
darkkhaki: [189, 183, 107],
|
||||||
|
darkmagenta: [139, 0, 139],
|
||||||
|
darkolivegreen: [85, 107, 47],
|
||||||
|
darkorange: [255, 140, 0],
|
||||||
|
darkorchid: [153, 50, 204],
|
||||||
|
darkred: [139, 0, 0],
|
||||||
|
darksalmon: [233, 150, 122],
|
||||||
|
darkviolet: [148, 0, 211],
|
||||||
|
fuchsia: [255, 0, 255],
|
||||||
|
gold: [255, 215, 0],
|
||||||
|
green: [0, 128, 0],
|
||||||
|
indigo: [75, 0, 130],
|
||||||
|
khaki: [240, 230, 140],
|
||||||
|
lightblue: [173, 216, 230],
|
||||||
|
lightcyan: [224, 255, 255],
|
||||||
|
lightgreen: [144, 238, 144],
|
||||||
|
lightgrey: [211, 211, 211],
|
||||||
|
lightpink: [255, 182, 193],
|
||||||
|
lightyellow: [255, 255, 224],
|
||||||
|
lime: [0, 255, 0],
|
||||||
|
magenta: [255, 0, 255],
|
||||||
|
maroon: [128, 0, 0],
|
||||||
|
navy: [0, 0, 128],
|
||||||
|
olive: [128, 128, 0],
|
||||||
|
orange: [255, 165, 0],
|
||||||
|
pink: [255, 192, 203],
|
||||||
|
purple: [128, 0, 128],
|
||||||
|
violet: [128, 0, 128],
|
||||||
|
red: [255, 0, 0],
|
||||||
|
silver: [192, 192, 192],
|
||||||
|
white: [255, 255, 255],
|
||||||
|
yellow: [255, 255, 0]
|
||||||
|
};
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
/*
|
||||||
|
Axis label plugin for flot
|
||||||
|
|
||||||
|
Derived from:
|
||||||
|
Axis Labels Plugin for flot.
|
||||||
|
http://github.com/markrcote/flot-axislabels
|
||||||
|
|
||||||
|
Original code is Copyright (c) 2010 Xuan Luo.
|
||||||
|
Original code was released under the GPLv3 license by Xuan Luo, September 2010.
|
||||||
|
Original code was rereleased under the MIT license by Xuan Luo, April 2012.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation 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 furnished 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 FOR 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
axisLabels: {
|
||||||
|
show: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function AxisLabel(axisName, position, padding, placeholder, axisLabel, surface) {
|
||||||
|
this.axisName = axisName;
|
||||||
|
this.position = position;
|
||||||
|
this.padding = padding;
|
||||||
|
this.placeholder = placeholder;
|
||||||
|
this.axisLabel = axisLabel;
|
||||||
|
this.surface = surface;
|
||||||
|
this.width = 0;
|
||||||
|
this.height = 0;
|
||||||
|
this.elem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AxisLabel.prototype.calculateSize = function() {
|
||||||
|
var axisId = this.axisName + 'Label',
|
||||||
|
layerId = axisId + 'Layer',
|
||||||
|
className = axisId + ' axisLabels';
|
||||||
|
|
||||||
|
var info = this.surface.getTextInfo(layerId, this.axisLabel, className);
|
||||||
|
this.labelWidth = info.width;
|
||||||
|
this.labelHeight = info.height;
|
||||||
|
|
||||||
|
if (this.position === 'left' || this.position === 'right') {
|
||||||
|
this.width = this.labelHeight + this.padding;
|
||||||
|
this.height = 0;
|
||||||
|
} else {
|
||||||
|
this.width = 0;
|
||||||
|
this.height = this.labelHeight + this.padding;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AxisLabel.prototype.transforms = function(degrees, x, y, svgLayer) {
|
||||||
|
var transforms = [], translate, rotate;
|
||||||
|
if (x !== 0 || y !== 0) {
|
||||||
|
translate = svgLayer.createSVGTransform();
|
||||||
|
translate.setTranslate(x, y);
|
||||||
|
transforms.push(translate);
|
||||||
|
}
|
||||||
|
if (degrees !== 0) {
|
||||||
|
rotate = svgLayer.createSVGTransform();
|
||||||
|
var centerX = Math.round(this.labelWidth / 2),
|
||||||
|
centerY = 0;
|
||||||
|
rotate.setRotate(degrees, centerX, centerY);
|
||||||
|
transforms.push(rotate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return transforms;
|
||||||
|
};
|
||||||
|
|
||||||
|
AxisLabel.prototype.calculateOffsets = function(box) {
|
||||||
|
var offsets = {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
degrees: 0
|
||||||
|
};
|
||||||
|
if (this.position === 'bottom') {
|
||||||
|
offsets.x = box.left + box.width / 2 - this.labelWidth / 2;
|
||||||
|
offsets.y = box.top + box.height - this.labelHeight;
|
||||||
|
} else if (this.position === 'top') {
|
||||||
|
offsets.x = box.left + box.width / 2 - this.labelWidth / 2;
|
||||||
|
offsets.y = box.top;
|
||||||
|
} else if (this.position === 'left') {
|
||||||
|
offsets.degrees = -90;
|
||||||
|
offsets.x = box.left - this.labelWidth / 2;
|
||||||
|
offsets.y = box.height / 2 + box.top;
|
||||||
|
} else if (this.position === 'right') {
|
||||||
|
offsets.degrees = 90;
|
||||||
|
offsets.x = box.left + box.width - this.labelWidth / 2;
|
||||||
|
offsets.y = box.height / 2 + box.top;
|
||||||
|
}
|
||||||
|
offsets.x = Math.round(offsets.x);
|
||||||
|
offsets.y = Math.round(offsets.y);
|
||||||
|
|
||||||
|
return offsets;
|
||||||
|
};
|
||||||
|
|
||||||
|
AxisLabel.prototype.cleanup = function() {
|
||||||
|
var axisId = this.axisName + 'Label',
|
||||||
|
layerId = axisId + 'Layer',
|
||||||
|
className = axisId + ' axisLabels';
|
||||||
|
this.surface.removeText(layerId, 0, 0, this.axisLabel, className);
|
||||||
|
};
|
||||||
|
|
||||||
|
AxisLabel.prototype.draw = function(box) {
|
||||||
|
var axisId = this.axisName + 'Label',
|
||||||
|
layerId = axisId + 'Layer',
|
||||||
|
className = axisId + ' axisLabels',
|
||||||
|
offsets = this.calculateOffsets(box),
|
||||||
|
style = {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '',
|
||||||
|
right: '',
|
||||||
|
display: 'inline-block',
|
||||||
|
'white-space': 'nowrap'
|
||||||
|
};
|
||||||
|
|
||||||
|
var layer = this.surface.getSVGLayer(layerId);
|
||||||
|
var transforms = this.transforms(offsets.degrees, offsets.x, offsets.y, layer.parentNode);
|
||||||
|
|
||||||
|
this.surface.addText(layerId, 0, 0, this.axisLabel, className, undefined, undefined, undefined, undefined, transforms);
|
||||||
|
this.surface.render();
|
||||||
|
Object.keys(style).forEach(function(key) {
|
||||||
|
layer.style[key] = style[key];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processOptions.push(function(plot, options) {
|
||||||
|
if (!options.axisLabels.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var axisLabels = {};
|
||||||
|
var defaultPadding = 2; // padding between axis and tick labels
|
||||||
|
|
||||||
|
plot.hooks.axisReserveSpace.push(function(plot, axis) {
|
||||||
|
var opts = axis.options;
|
||||||
|
var axisName = axis.direction + axis.n;
|
||||||
|
|
||||||
|
axis.labelHeight += axis.boxPosition.centerY;
|
||||||
|
axis.labelWidth += axis.boxPosition.centerX;
|
||||||
|
|
||||||
|
if (!opts || !opts.axisLabel || !axis.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var padding = opts.axisLabelPadding === undefined
|
||||||
|
? defaultPadding
|
||||||
|
: opts.axisLabelPadding;
|
||||||
|
|
||||||
|
var axisLabel = axisLabels[axisName];
|
||||||
|
if (!axisLabel) {
|
||||||
|
axisLabel = new AxisLabel(axisName,
|
||||||
|
opts.position, padding,
|
||||||
|
plot.getPlaceholder()[0], opts.axisLabel, plot.getSurface());
|
||||||
|
axisLabels[axisName] = axisLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
axisLabel.calculateSize();
|
||||||
|
|
||||||
|
// Incrementing the sizes of the tick labels.
|
||||||
|
axis.labelHeight += axisLabel.height;
|
||||||
|
axis.labelWidth += axisLabel.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO - use the drawAxis hook
|
||||||
|
plot.hooks.draw.push(function(plot, ctx) {
|
||||||
|
$.each(plot.getAxes(), function(flotAxisName, axis) {
|
||||||
|
var opts = axis.options;
|
||||||
|
if (!opts || !opts.axisLabel || !axis.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var axisName = axis.direction + axis.n;
|
||||||
|
axisLabels[axisName].draw(axis.box);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.shutdown.push(function(plot, eventHolder) {
|
||||||
|
for (var axisName in axisLabels) {
|
||||||
|
axisLabels[axisName].cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'axisLabels',
|
||||||
|
version: '3.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/** ## jquery.flot.browser.js
|
||||||
|
|
||||||
|
This plugin is used to make available some browser-related utility functions.
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var browser = {
|
||||||
|
/**
|
||||||
|
- getPageXY(e)
|
||||||
|
|
||||||
|
Calculates the pageX and pageY using the screenX, screenY properties of the event
|
||||||
|
and the scrolling of the page. This is needed because the pageX and pageY
|
||||||
|
properties of the event are not correct while running tests in Edge. */
|
||||||
|
getPageXY: function (e) {
|
||||||
|
// This code is inspired from https://stackoverflow.com/a/3464890
|
||||||
|
var doc = document.documentElement,
|
||||||
|
pageX = e.clientX + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
|
||||||
|
pageY = e.clientY + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
|
||||||
|
return { X: pageX, Y: pageY };
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
- getPixelRatio(context)
|
||||||
|
|
||||||
|
This function returns the current pixel ratio defined by the product of desktop
|
||||||
|
zoom and page zoom.
|
||||||
|
Additional info: https://www.html5rocks.com/en/tutorials/canvas/hidpi/
|
||||||
|
*/
|
||||||
|
getPixelRatio: function(context) {
|
||||||
|
var devicePixelRatio = window.devicePixelRatio || 1,
|
||||||
|
backingStoreRatio =
|
||||||
|
context.webkitBackingStorePixelRatio ||
|
||||||
|
context.mozBackingStorePixelRatio ||
|
||||||
|
context.msBackingStorePixelRatio ||
|
||||||
|
context.oBackingStorePixelRatio ||
|
||||||
|
context.backingStorePixelRatio || 1;
|
||||||
|
return devicePixelRatio / backingStoreRatio;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
- isSafari, isMobileSafari, isOpera, isFirefox, isIE, isEdge, isChrome, isBlink
|
||||||
|
|
||||||
|
This is a collection of functions, used to check if the code is running in a
|
||||||
|
particular browser or Javascript engine.
|
||||||
|
*/
|
||||||
|
isSafari: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
// Safari 3.0+ "[object HTMLElementConstructor]"
|
||||||
|
return /constructor/i.test(window.top.HTMLElement) || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window.top['safari'] || (typeof window.top.safari !== 'undefined' && window.top.safari.pushNotification));
|
||||||
|
},
|
||||||
|
|
||||||
|
isMobileSafari: function() {
|
||||||
|
//isMobileSafari adapted from https://stackoverflow.com/questions/3007480/determine-if-user-navigated-from-mobile-safari
|
||||||
|
return navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
|
||||||
|
},
|
||||||
|
|
||||||
|
isOpera: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
//Opera 8.0+
|
||||||
|
return (!!window.opr && !!opr.addons) || !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
isFirefox: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
// Firefox 1.0+
|
||||||
|
return typeof InstallTrigger !== 'undefined';
|
||||||
|
},
|
||||||
|
|
||||||
|
isIE: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
// Internet Explorer 6-11
|
||||||
|
return /*@cc_on!@*/false || !!document.documentMode;
|
||||||
|
},
|
||||||
|
|
||||||
|
isEdge: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
// Edge 20+
|
||||||
|
return !browser.isIE() && !!window.StyleMedia;
|
||||||
|
},
|
||||||
|
|
||||||
|
isChrome: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
// Chrome 1+
|
||||||
|
return !!window.chrome && !!window.chrome.webstore;
|
||||||
|
},
|
||||||
|
|
||||||
|
isBlink: function() {
|
||||||
|
// *** https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser
|
||||||
|
return (browser.isChrome() || browser.isOpera()) && !!window.CSS;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.browser = browser;
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/* Flot plugin for plotting textual data or categories.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin
|
||||||
|
allows you to plot such a dataset directly.
|
||||||
|
|
||||||
|
To enable it, you must specify mode: "categories" on the axis with the textual
|
||||||
|
labels, e.g.
|
||||||
|
|
||||||
|
$.plot("#placeholder", data, { xaxis: { mode: "categories" } });
|
||||||
|
|
||||||
|
By default, the labels are ordered as they are met in the data series. If you
|
||||||
|
need a different ordering, you can specify "categories" on the axis options
|
||||||
|
and list the categories there:
|
||||||
|
|
||||||
|
xaxis: {
|
||||||
|
mode: "categories",
|
||||||
|
categories: ["February", "March", "April"]
|
||||||
|
}
|
||||||
|
|
||||||
|
If you need to customize the distances between the categories, you can specify
|
||||||
|
"categories" as an object mapping labels to values
|
||||||
|
|
||||||
|
xaxis: {
|
||||||
|
mode: "categories",
|
||||||
|
categories: { "February": 1, "March": 3, "April": 4 }
|
||||||
|
}
|
||||||
|
|
||||||
|
If you don't specify all categories, the remaining categories will be numbered
|
||||||
|
from the max value plus 1 (with a spacing of 1 between each).
|
||||||
|
|
||||||
|
Internally, the plugin works by transforming the input data through an auto-
|
||||||
|
generated mapping where the first category becomes 0, the second 1, etc.
|
||||||
|
Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this
|
||||||
|
is visible in hover and click events that return numbers rather than the
|
||||||
|
category labels). The plugin also overrides the tick generator to spit out the
|
||||||
|
categories as ticks instead of the values.
|
||||||
|
|
||||||
|
If you need to map a value back to its label, the mapping is always accessible
|
||||||
|
as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
xaxis: {
|
||||||
|
categories: null
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
categories: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function processRawData(plot, series, data, datapoints) {
|
||||||
|
// if categories are enabled, we need to disable
|
||||||
|
// auto-transformation to numbers so the strings are intact
|
||||||
|
// for later processing
|
||||||
|
|
||||||
|
var xCategories = series.xaxis.options.mode === "categories",
|
||||||
|
yCategories = series.yaxis.options.mode === "categories";
|
||||||
|
|
||||||
|
if (!(xCategories || yCategories)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = datapoints.format;
|
||||||
|
|
||||||
|
if (!format) {
|
||||||
|
// FIXME: auto-detection should really not be defined here
|
||||||
|
var s = series;
|
||||||
|
format = [];
|
||||||
|
format.push({ x: true, number: true, required: true, computeRange: true});
|
||||||
|
format.push({ y: true, number: true, required: true, computeRange: true });
|
||||||
|
|
||||||
|
if (s.bars.show || (s.lines.show && s.lines.fill)) {
|
||||||
|
var autoScale = !!((s.bars.show && s.bars.zero) || (s.lines.show && s.lines.zero));
|
||||||
|
format.push({ y: true, number: true, required: false, defaultValue: 0, computeRange: autoScale });
|
||||||
|
if (s.bars.horizontal) {
|
||||||
|
delete format[format.length - 1].y;
|
||||||
|
format[format.length - 1].x = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var m = 0; m < format.length; ++m) {
|
||||||
|
if (format[m].x && xCategories) {
|
||||||
|
format[m].number = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (format[m].y && yCategories) {
|
||||||
|
format[m].number = false;
|
||||||
|
format[m].computeRange = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNextIndex(categories) {
|
||||||
|
var index = -1;
|
||||||
|
|
||||||
|
for (var v in categories) {
|
||||||
|
if (categories[v] > index) {
|
||||||
|
index = categories[v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function categoriesTickGenerator(axis) {
|
||||||
|
var res = [];
|
||||||
|
for (var label in axis.categories) {
|
||||||
|
var v = axis.categories[label];
|
||||||
|
if (v >= axis.min && v <= axis.max) {
|
||||||
|
res.push([v, label]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sort(function (a, b) { return a[0] - b[0]; });
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupCategoriesForAxis(series, axis, datapoints) {
|
||||||
|
if (series[axis].options.mode !== "categories") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!series[axis].categories) {
|
||||||
|
// parse options
|
||||||
|
var c = {}, o = series[axis].options.categories || {};
|
||||||
|
if ($.isArray(o)) {
|
||||||
|
for (var i = 0; i < o.length; ++i) {
|
||||||
|
c[o[i]] = i;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (var v in o) {
|
||||||
|
c[v] = o[v];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
series[axis].categories = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix ticks
|
||||||
|
if (!series[axis].options.ticks) {
|
||||||
|
series[axis].options.ticks = categoriesTickGenerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformPointsOnAxis(datapoints, axis, series[axis].categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformPointsOnAxis(datapoints, axis, categories) {
|
||||||
|
// go through the points, transforming them
|
||||||
|
var points = datapoints.points,
|
||||||
|
ps = datapoints.pointsize,
|
||||||
|
format = datapoints.format,
|
||||||
|
formatColumn = axis.charAt(0),
|
||||||
|
index = getNextIndex(categories);
|
||||||
|
|
||||||
|
for (var i = 0; i < points.length; i += ps) {
|
||||||
|
if (points[i] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var m = 0; m < ps; ++m) {
|
||||||
|
var val = points[i + m];
|
||||||
|
|
||||||
|
if (val == null || !format[m][formatColumn]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(val in categories)) {
|
||||||
|
categories[val] = index;
|
||||||
|
++index;
|
||||||
|
}
|
||||||
|
|
||||||
|
points[i + m] = categories[val];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDatapoints(plot, series, datapoints) {
|
||||||
|
setupCategoriesForAxis(series, "xaxis", datapoints);
|
||||||
|
setupCategoriesForAxis(series, "yaxis", datapoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processRawData.push(processRawData);
|
||||||
|
plot.hooks.processDatapoints.push(processDatapoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'categories',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
/** ## jquery.flot.composeImages.js
|
||||||
|
|
||||||
|
This plugin is used to expose a function used to overlap several canvases and
|
||||||
|
SVGs, for the purpose of creating a snaphot out of them.
|
||||||
|
|
||||||
|
### When composeImages is used:
|
||||||
|
When multiple canvases and SVGs have to be overlapped into a single image
|
||||||
|
and their offset on the page, must be preserved.
|
||||||
|
|
||||||
|
### Where can be used:
|
||||||
|
In creating a downloadable snapshot of the plots, axes, cursors etc of a graph.
|
||||||
|
|
||||||
|
### How it works:
|
||||||
|
The entry point is composeImages function. It expects an array of objects,
|
||||||
|
which should be either canvases or SVGs (or a mix). It does a prevalidation
|
||||||
|
of them, by verifying if they will be usable or not, later in the flow.
|
||||||
|
After selecting only usable sources, it passes them to getGenerateTempImg
|
||||||
|
function, which generates temporary images out of them. This function
|
||||||
|
expects that some of the passed sources (canvas or SVG) may still have
|
||||||
|
problems being converted to an image and makes sure the promises system,
|
||||||
|
used by composeImages function, moves forward. As an example, SVGs with
|
||||||
|
missing information from header or with unsupported content, may lead to
|
||||||
|
failure in generating the temporary image. Temporary images are required
|
||||||
|
mostly on extracting content from SVGs, but this is also where the x/y
|
||||||
|
offsets are extracted for each image which will be added. For SVGs in
|
||||||
|
particular, their CSS rules have to be applied.
|
||||||
|
After all temporary images are generated, they are overlapped using
|
||||||
|
getExecuteImgComposition function. This is where the destination canvas
|
||||||
|
is set to the proper dimensions. It is then output by composeImages.
|
||||||
|
This function returns a promise, which can be used to wait for the whole
|
||||||
|
composition process. It requires to be asynchronous, because this is how
|
||||||
|
temporary images load their data.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
"use strict";
|
||||||
|
const GENERALFAILURECALLBACKERROR = -100; //simply a negative number
|
||||||
|
const SUCCESSFULIMAGEPREPARATION = 0;
|
||||||
|
const EMPTYARRAYOFIMAGESOURCES = -1;
|
||||||
|
const NEGATIVEIMAGESIZE = -2;
|
||||||
|
var pixelRatio = 1;
|
||||||
|
var browser = $.plot.browser;
|
||||||
|
var getPixelRatio = browser.getPixelRatio;
|
||||||
|
|
||||||
|
function composeImages(canvasOrSvgSources, destinationCanvas) {
|
||||||
|
var validCanvasOrSvgSources = canvasOrSvgSources.filter(isValidSource);
|
||||||
|
pixelRatio = getPixelRatio(destinationCanvas.getContext('2d'));
|
||||||
|
|
||||||
|
var allImgCompositionPromises = validCanvasOrSvgSources.map(function(validCanvasOrSvgSource) {
|
||||||
|
var tempImg = new Image();
|
||||||
|
var currentPromise = new Promise(getGenerateTempImg(tempImg, validCanvasOrSvgSource));
|
||||||
|
return currentPromise;
|
||||||
|
});
|
||||||
|
|
||||||
|
var lastPromise = Promise.all(allImgCompositionPromises).then(getExecuteImgComposition(destinationCanvas), failureCallback);
|
||||||
|
return lastPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidSource(canvasOrSvgSource) {
|
||||||
|
var isValidFromCanvas = true;
|
||||||
|
var isValidFromContent = true;
|
||||||
|
if ((canvasOrSvgSource === null) || (canvasOrSvgSource === undefined)) {
|
||||||
|
isValidFromContent = false;
|
||||||
|
} else {
|
||||||
|
if (canvasOrSvgSource.tagName === 'CANVAS') {
|
||||||
|
if ((canvasOrSvgSource.getBoundingClientRect().right === canvasOrSvgSource.getBoundingClientRect().left) ||
|
||||||
|
(canvasOrSvgSource.getBoundingClientRect().bottom === canvasOrSvgSource.getBoundingClientRect().top)) {
|
||||||
|
isValidFromCanvas = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isValidFromContent && isValidFromCanvas && (window.getComputedStyle(canvasOrSvgSource).visibility === 'visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGenerateTempImg(tempImg, canvasOrSvgSource) {
|
||||||
|
tempImg.sourceDescription = '<info className="' + canvasOrSvgSource.className + '" tagName="' + canvasOrSvgSource.tagName + '" id="' + canvasOrSvgSource.id + '">';
|
||||||
|
tempImg.sourceComponent = canvasOrSvgSource;
|
||||||
|
|
||||||
|
return function doGenerateTempImg(successCallbackFunc, failureCallbackFunc) {
|
||||||
|
tempImg.onload = function(evt) {
|
||||||
|
tempImg.successfullyLoaded = true;
|
||||||
|
successCallbackFunc(tempImg);
|
||||||
|
};
|
||||||
|
|
||||||
|
tempImg.onabort = function(evt) {
|
||||||
|
tempImg.successfullyLoaded = false;
|
||||||
|
console.log('Can\'t generate temp image from ' + tempImg.sourceDescription + '. It is possible that it is missing some properties or its content is not supported by this browser. Source component:', tempImg.sourceComponent);
|
||||||
|
successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images
|
||||||
|
};
|
||||||
|
|
||||||
|
tempImg.onerror = function(evt) {
|
||||||
|
tempImg.successfullyLoaded = false;
|
||||||
|
console.log('Can\'t generate temp image from ' + tempImg.sourceDescription + '. It is possible that it is missing some properties or its content is not supported by this browser. Source component:', tempImg.sourceComponent);
|
||||||
|
successCallbackFunc(tempImg); //call successCallback, to allow snapshot of all working images
|
||||||
|
};
|
||||||
|
|
||||||
|
generateTempImageFromCanvasOrSvg(canvasOrSvgSource, tempImg);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExecuteImgComposition(destinationCanvas) {
|
||||||
|
return function executeImgComposition(tempImgs) {
|
||||||
|
var compositionResult = copyImgsToCanvas(tempImgs, destinationCanvas);
|
||||||
|
return compositionResult;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCanvasToImg(canvas, img) {
|
||||||
|
img.src = canvas.toDataURL('image/png');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCSSRules(document) {
|
||||||
|
var styleSheets = document.styleSheets,
|
||||||
|
rulesList = [];
|
||||||
|
for (var i = 0; i < styleSheets.length; i++) {
|
||||||
|
// CORS requests for style sheets throw and an exception on Chrome > 64
|
||||||
|
try {
|
||||||
|
// in Chrome, the external CSS files are empty when the page is directly loaded from disk
|
||||||
|
var rules = styleSheets[i].cssRules || [];
|
||||||
|
for (var j = 0; j < rules.length; j++) {
|
||||||
|
var rule = rules[j];
|
||||||
|
rulesList.push(rule.cssText);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to get some css rules');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rulesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
function embedCSSRulesInSVG(rules, svg) {
|
||||||
|
var text = [
|
||||||
|
'<svg class="snapshot ' + svg.classList + '" width="' + svg.width.baseVal.value * pixelRatio + '" height="' + svg.height.baseVal.value * pixelRatio + '" viewBox="0 0 ' + svg.width.baseVal.value + ' ' + svg.height.baseVal.value + '" xmlns="http://www.w3.org/2000/svg">',
|
||||||
|
'<style>',
|
||||||
|
'/* <![CDATA[ */',
|
||||||
|
rules.join('\n'),
|
||||||
|
'/* ]]> */',
|
||||||
|
'</style>',
|
||||||
|
svg.innerHTML,
|
||||||
|
'</svg>'
|
||||||
|
].join('\n');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySVGToImgMostBrowsers(svg, img) {
|
||||||
|
var rules = getCSSRules(document),
|
||||||
|
source = embedCSSRulesInSVG(rules, svg);
|
||||||
|
|
||||||
|
source = patchSVGSource(source);
|
||||||
|
|
||||||
|
var blob = new Blob([source], {type: "image/svg+xml;charset=utf-8"}),
|
||||||
|
domURL = self.URL || self.webkitURL || self,
|
||||||
|
url = domURL.createObjectURL(blob);
|
||||||
|
img.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySVGToImgSafari(svg, img) {
|
||||||
|
// Use this method to convert a string buffer array to a binary string.
|
||||||
|
// Do so by breaking up large strings into smaller substrings; this is necessary to avoid the
|
||||||
|
// "maximum call stack size exceeded" exception that can happen when calling 'String.fromCharCode.apply'
|
||||||
|
// with a very long array.
|
||||||
|
function buildBinaryString (arrayBuffer) {
|
||||||
|
var binaryString = "";
|
||||||
|
const utf8Array = new Uint8Array(arrayBuffer);
|
||||||
|
const blockSize = 16384;
|
||||||
|
for (var i = 0; i < utf8Array.length; i = i + blockSize) {
|
||||||
|
const binarySubString = String.fromCharCode.apply(null, utf8Array.subarray(i, i + blockSize));
|
||||||
|
binaryString = binaryString + binarySubString;
|
||||||
|
}
|
||||||
|
return binaryString;
|
||||||
|
};
|
||||||
|
|
||||||
|
var rules = getCSSRules(document),
|
||||||
|
source = embedCSSRulesInSVG(rules, svg),
|
||||||
|
data,
|
||||||
|
utf8BinaryString;
|
||||||
|
|
||||||
|
source = patchSVGSource(source);
|
||||||
|
|
||||||
|
// Encode the string as UTF-8 and convert it to a binary string. The UTF-8 encoding is required to
|
||||||
|
// capture unicode characters correctly.
|
||||||
|
utf8BinaryString = buildBinaryString(new (TextEncoder || TextEncoderLite)('utf-8').encode(source));
|
||||||
|
|
||||||
|
data = "data:image/svg+xml;base64," + btoa(utf8BinaryString);
|
||||||
|
img.src = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function patchSVGSource(svgSource) {
|
||||||
|
var source = '';
|
||||||
|
//add name spaces.
|
||||||
|
if (!svgSource.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
|
||||||
|
source = svgSource.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
||||||
|
}
|
||||||
|
if (!svgSource.match(/^<svg[^>]+"http:\/\/www\.w3\.org\/1999\/xlink"/)) {
|
||||||
|
source = svgSource.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
|
||||||
|
}
|
||||||
|
|
||||||
|
//add xml declaration
|
||||||
|
return '<?xml version="1.0" standalone="no"?>\r\n' + source;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copySVGToImg(svg, img) {
|
||||||
|
if (browser.isSafari() || browser.isMobileSafari()) {
|
||||||
|
copySVGToImgSafari(svg, img);
|
||||||
|
} else {
|
||||||
|
copySVGToImgMostBrowsers(svg, img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function adaptDestSizeToZoom(destinationCanvas, sources) {
|
||||||
|
function containsSVGs(source) {
|
||||||
|
return source.srcImgTagName === 'svg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sources.find(containsSVGs) !== undefined) {
|
||||||
|
if (pixelRatio < 1) {
|
||||||
|
destinationCanvas.width = destinationCanvas.width * pixelRatio;
|
||||||
|
destinationCanvas.height = destinationCanvas.height * pixelRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareImagesToBeComposed(sources, destination) {
|
||||||
|
var result = SUCCESSFULIMAGEPREPARATION;
|
||||||
|
if (sources.length === 0) {
|
||||||
|
result = EMPTYARRAYOFIMAGESOURCES; //nothing to do if called without sources
|
||||||
|
} else {
|
||||||
|
var minX = sources[0].genLeft;
|
||||||
|
var minY = sources[0].genTop;
|
||||||
|
var maxX = sources[0].genRight;
|
||||||
|
var maxY = sources[0].genBottom;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
for (i = 1; i < sources.length; i++) {
|
||||||
|
if (minX > sources[i].genLeft) {
|
||||||
|
minX = sources[i].genLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minY > sources[i].genTop) {
|
||||||
|
minY = sources[i].genTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 1; i < sources.length; i++) {
|
||||||
|
if (maxX < sources[i].genRight) {
|
||||||
|
maxX = sources[i].genRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxY < sources[i].genBottom) {
|
||||||
|
maxY = sources[i].genBottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((maxX - minX <= 0) || (maxY - minY <= 0)) {
|
||||||
|
result = NEGATIVEIMAGESIZE; //this might occur on hidden images
|
||||||
|
} else {
|
||||||
|
destination.width = Math.round(maxX - minX);
|
||||||
|
destination.height = Math.round(maxY - minY);
|
||||||
|
|
||||||
|
for (i = 0; i < sources.length; i++) {
|
||||||
|
sources[i].xCompOffset = sources[i].genLeft - minX;
|
||||||
|
sources[i].yCompOffset = sources[i].genTop - minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
adaptDestSizeToZoom(destination, sources);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyImgsToCanvas(sources, destination) {
|
||||||
|
var prepareImagesResult = prepareImagesToBeComposed(sources, destination);
|
||||||
|
if (prepareImagesResult === SUCCESSFULIMAGEPREPARATION) {
|
||||||
|
var destinationCtx = destination.getContext('2d');
|
||||||
|
|
||||||
|
for (var i = 0; i < sources.length; i++) {
|
||||||
|
if (sources[i].successfullyLoaded === true) {
|
||||||
|
destinationCtx.drawImage(sources[i], sources[i].xCompOffset * pixelRatio, sources[i].yCompOffset * pixelRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prepareImagesResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
function adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg) {
|
||||||
|
destImg.genLeft = srcCanvasOrSvg.getBoundingClientRect().left;
|
||||||
|
destImg.genTop = srcCanvasOrSvg.getBoundingClientRect().top;
|
||||||
|
|
||||||
|
if (srcCanvasOrSvg.tagName === 'CANVAS') {
|
||||||
|
destImg.genRight = destImg.genLeft + srcCanvasOrSvg.width;
|
||||||
|
destImg.genBottom = destImg.genTop + srcCanvasOrSvg.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (srcCanvasOrSvg.tagName === 'svg') {
|
||||||
|
destImg.genRight = srcCanvasOrSvg.getBoundingClientRect().right;
|
||||||
|
destImg.genBottom = srcCanvasOrSvg.getBoundingClientRect().bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTempImageFromCanvasOrSvg(srcCanvasOrSvg, destImg) {
|
||||||
|
if (srcCanvasOrSvg.tagName === 'CANVAS') {
|
||||||
|
copyCanvasToImg(srcCanvasOrSvg, destImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (srcCanvasOrSvg.tagName === 'svg') {
|
||||||
|
copySVGToImg(srcCanvasOrSvg, destImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
destImg.srcImgTagName = srcCanvasOrSvg.tagName;
|
||||||
|
adnotateDestImgWithBoundingClientRect(srcCanvasOrSvg, destImg);
|
||||||
|
}
|
||||||
|
|
||||||
|
function failureCallback() {
|
||||||
|
return GENERALFAILURECALLBACKERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// used for testing
|
||||||
|
$.plot.composeImages = composeImages;
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
// used to extend the public API of the plot
|
||||||
|
plot.composeImages = composeImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
name: 'composeImages',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/* Flot plugin for showing crosshairs when the mouse hovers over the plot.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
|
||||||
|
crosshair: {
|
||||||
|
mode: null or "x" or "y" or "xy"
|
||||||
|
color: color
|
||||||
|
lineWidth: number
|
||||||
|
}
|
||||||
|
|
||||||
|
Set the mode to one of "x", "y" or "xy". The "x" mode enables a vertical
|
||||||
|
crosshair that lets you trace the values on the x axis, "y" enables a
|
||||||
|
horizontal crosshair and "xy" enables them both. "color" is the color of the
|
||||||
|
crosshair (default is "rgba(170, 0, 0, 0.80)"), "lineWidth" is the width of
|
||||||
|
the drawn lines (default is 1).
|
||||||
|
|
||||||
|
The plugin also adds four public methods:
|
||||||
|
|
||||||
|
- setCrosshair( pos )
|
||||||
|
|
||||||
|
Set the position of the crosshair. Note that this is cleared if the user
|
||||||
|
moves the mouse. "pos" is in coordinates of the plot and should be on the
|
||||||
|
form { x: xpos, y: ypos } (you can use x2/x3/... if you're using multiple
|
||||||
|
axes), which is coincidentally the same format as what you get from a
|
||||||
|
"plothover" event. If "pos" is null, the crosshair is cleared.
|
||||||
|
|
||||||
|
- clearCrosshair()
|
||||||
|
|
||||||
|
Clear the crosshair.
|
||||||
|
|
||||||
|
- lockCrosshair(pos)
|
||||||
|
|
||||||
|
Cause the crosshair to lock to the current location, no longer updating if
|
||||||
|
the user moves the mouse. Optionally supply a position (passed on to
|
||||||
|
setCrosshair()) to move it to.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
var myFlot = $.plot( $("#graph"), ..., { crosshair: { mode: "x" } } };
|
||||||
|
$("#graph").bind( "plothover", function ( evt, position, item ) {
|
||||||
|
if ( item ) {
|
||||||
|
// Lock the crosshair to the data point being hovered
|
||||||
|
myFlot.lockCrosshair({
|
||||||
|
x: item.datapoint[ 0 ],
|
||||||
|
y: item.datapoint[ 1 ]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Return normal crosshair operation
|
||||||
|
myFlot.unlockCrosshair();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
- unlockCrosshair()
|
||||||
|
|
||||||
|
Free the crosshair to move again after locking it.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
crosshair: {
|
||||||
|
mode: null, // one of null, "x", "y" or "xy",
|
||||||
|
color: "rgba(170, 0, 0, 0.80)",
|
||||||
|
lineWidth: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
// position of crosshair in pixels
|
||||||
|
var crosshair = {x: -1, y: -1, locked: false, highlighted: false};
|
||||||
|
|
||||||
|
plot.setCrosshair = function setCrosshair(pos) {
|
||||||
|
if (!pos) {
|
||||||
|
crosshair.x = -1;
|
||||||
|
} else {
|
||||||
|
var o = plot.p2c(pos);
|
||||||
|
crosshair.x = Math.max(0, Math.min(o.left, plot.width()));
|
||||||
|
crosshair.y = Math.max(0, Math.min(o.top, plot.height()));
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.clearCrosshair = plot.setCrosshair; // passes null for pos
|
||||||
|
|
||||||
|
plot.lockCrosshair = function lockCrosshair(pos) {
|
||||||
|
if (pos) {
|
||||||
|
plot.setCrosshair(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshair.locked = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.unlockCrosshair = function unlockCrosshair() {
|
||||||
|
crosshair.locked = false;
|
||||||
|
crosshair.rect = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function onMouseOut(e) {
|
||||||
|
if (crosshair.locked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (crosshair.x !== -1) {
|
||||||
|
crosshair.x = -1;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
var offset = plot.offset();
|
||||||
|
if (crosshair.locked) {
|
||||||
|
var mouseX = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
|
||||||
|
var mouseY = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
|
||||||
|
|
||||||
|
if ((mouseX > crosshair.x - 4) && (mouseX < crosshair.x + 4) && (mouseY > crosshair.y - 4) && (mouseY < crosshair.y + 4)) {
|
||||||
|
if (!crosshair.highlighted) {
|
||||||
|
crosshair.highlighted = true;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (crosshair.highlighted) {
|
||||||
|
crosshair.highlighted = false;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plot.getSelection && plot.getSelection()) {
|
||||||
|
crosshair.x = -1; // hide the crosshair while selecting
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
crosshair.x = Math.max(0, Math.min(e.pageX - offset.left, plot.width()));
|
||||||
|
crosshair.y = Math.max(0, Math.min(e.pageY - offset.top, plot.height()));
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function (plot, eventHolder) {
|
||||||
|
if (!plot.getOptions().crosshair.mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHolder.mouseout(onMouseOut);
|
||||||
|
eventHolder.mousemove(onMouseMove);
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function (plot, ctx) {
|
||||||
|
var c = plot.getOptions().crosshair;
|
||||||
|
if (!c.mode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
|
||||||
|
if (crosshair.x !== -1) {
|
||||||
|
var adj = plot.getOptions().crosshair.lineWidth % 2 ? 0.5 : 0;
|
||||||
|
|
||||||
|
ctx.strokeStyle = c.color;
|
||||||
|
ctx.lineWidth = c.lineWidth;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
if (c.mode.indexOf("x") !== -1) {
|
||||||
|
var drawX = Math.floor(crosshair.x) + adj;
|
||||||
|
ctx.moveTo(drawX, 0);
|
||||||
|
ctx.lineTo(drawX, plot.height());
|
||||||
|
}
|
||||||
|
if (c.mode.indexOf("y") !== -1) {
|
||||||
|
var drawY = Math.floor(crosshair.y) + adj;
|
||||||
|
ctx.moveTo(0, drawY);
|
||||||
|
ctx.lineTo(plot.width(), drawY);
|
||||||
|
}
|
||||||
|
if (crosshair.locked) {
|
||||||
|
if (crosshair.highlighted) ctx.fillStyle = 'orange';
|
||||||
|
else ctx.fillStyle = c.color;
|
||||||
|
ctx.fillRect(Math.floor(crosshair.x) + adj - 4, Math.floor(crosshair.y) + adj - 4, 8, 8);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.shutdown.push(function (plot, eventHolder) {
|
||||||
|
eventHolder.unbind("mouseout", onMouseOut);
|
||||||
|
eventHolder.unbind("mousemove", onMouseMove);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'crosshair',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,663 @@
|
|||||||
|
/**
|
||||||
|
## jquery.flot.drawSeries.js
|
||||||
|
|
||||||
|
This plugin is used by flot for drawing lines, plots, bars or area.
|
||||||
|
|
||||||
|
### Public methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function DrawSeries() {
|
||||||
|
function plotLine(datapoints, xoffset, yoffset, axisx, axisy, ctx, steps) {
|
||||||
|
var points = datapoints.points,
|
||||||
|
ps = datapoints.pointsize,
|
||||||
|
prevx = null,
|
||||||
|
prevy = null;
|
||||||
|
var x1 = 0.0,
|
||||||
|
y1 = 0.0,
|
||||||
|
x2 = 0.0,
|
||||||
|
y2 = 0.0,
|
||||||
|
mx = null,
|
||||||
|
my = null,
|
||||||
|
i = 0;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
for (i = ps; i < points.length; i += ps) {
|
||||||
|
x1 = points[i - ps];
|
||||||
|
y1 = points[i - ps + 1];
|
||||||
|
x2 = points[i];
|
||||||
|
y2 = points[i + 1];
|
||||||
|
|
||||||
|
if (x1 === null || x2 === null) {
|
||||||
|
mx = null;
|
||||||
|
my = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(x1) || isNaN(x2) || isNaN(y1) || isNaN(y2)) {
|
||||||
|
prevx = null;
|
||||||
|
prevy = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(steps){
|
||||||
|
if (mx !== null && my !== null) {
|
||||||
|
// if middle point exists, transfer p2 -> p1 and p1 -> mp
|
||||||
|
x2 = x1;
|
||||||
|
y2 = y1;
|
||||||
|
x1 = mx;
|
||||||
|
y1 = my;
|
||||||
|
|
||||||
|
// 'remove' middle point
|
||||||
|
mx = null;
|
||||||
|
my = null;
|
||||||
|
|
||||||
|
// subtract pointsize from i to have current point p1 handled again
|
||||||
|
i -= ps;
|
||||||
|
} else if (y1 !== y2 && x1 !== x2) {
|
||||||
|
// create a middle point
|
||||||
|
y2 = y1;
|
||||||
|
mx = x2;
|
||||||
|
my = y1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip with ymin
|
||||||
|
if (y1 <= y2 && y1 < axisy.min) {
|
||||||
|
if (y2 < axisy.min) {
|
||||||
|
// line segment is outside
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// compute new intersection point
|
||||||
|
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y1 = axisy.min;
|
||||||
|
} else if (y2 <= y1 && y2 < axisy.min) {
|
||||||
|
if (y1 < axisy.min) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y2 = axisy.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip with ymax
|
||||||
|
if (y1 >= y2 && y1 > axisy.max) {
|
||||||
|
if (y2 > axisy.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y1 = axisy.max;
|
||||||
|
} else if (y2 >= y1 && y2 > axisy.max) {
|
||||||
|
if (y1 > axisy.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y2 = axisy.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip with xmin
|
||||||
|
if (x1 <= x2 && x1 < axisx.min) {
|
||||||
|
if (x2 < axisx.min) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x1 = axisx.min;
|
||||||
|
} else if (x2 <= x1 && x2 < axisx.min) {
|
||||||
|
if (x1 < axisx.min) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x2 = axisx.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip with xmax
|
||||||
|
if (x1 >= x2 && x1 > axisx.max) {
|
||||||
|
if (x2 > axisx.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x1 = axisx.max;
|
||||||
|
} else if (x2 >= x1 && x2 > axisx.max) {
|
||||||
|
if (x1 > axisx.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x2 = axisx.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x1 !== prevx || y1 !== prevy) {
|
||||||
|
ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevx = x2;
|
||||||
|
prevy = y2;
|
||||||
|
ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function plotLineArea(datapoints, axisx, axisy, fillTowards, ctx, steps) {
|
||||||
|
var points = datapoints.points,
|
||||||
|
ps = datapoints.pointsize,
|
||||||
|
bottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min,
|
||||||
|
i = 0,
|
||||||
|
ypos = 1,
|
||||||
|
areaOpen = false,
|
||||||
|
segmentStart = 0,
|
||||||
|
segmentEnd = 0,
|
||||||
|
mx = null,
|
||||||
|
my = null;
|
||||||
|
|
||||||
|
// we process each segment in two turns, first forward
|
||||||
|
// direction to sketch out top, then once we hit the
|
||||||
|
// end we go backwards to sketch the bottom
|
||||||
|
while (true) {
|
||||||
|
if (ps > 0 && i > points.length + ps) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += ps; // ps is negative if going backwards
|
||||||
|
|
||||||
|
var x1 = points[i - ps],
|
||||||
|
y1 = points[i - ps + ypos],
|
||||||
|
x2 = points[i],
|
||||||
|
y2 = points[i + ypos];
|
||||||
|
|
||||||
|
if (ps === -2) {
|
||||||
|
/* going backwards and no value for the bottom provided in the series*/
|
||||||
|
y1 = y2 = bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areaOpen) {
|
||||||
|
if (ps > 0 && x1 != null && x2 == null) {
|
||||||
|
// at turning point
|
||||||
|
segmentEnd = i;
|
||||||
|
ps = -ps;
|
||||||
|
ypos = 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps < 0 && i === segmentStart + ps) {
|
||||||
|
// done with the reverse sweep
|
||||||
|
ctx.fill();
|
||||||
|
areaOpen = false;
|
||||||
|
ps = -ps;
|
||||||
|
ypos = 1;
|
||||||
|
i = segmentStart = segmentEnd + ps;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x1 == null || x2 == null) {
|
||||||
|
mx = null;
|
||||||
|
my = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(steps){
|
||||||
|
if (mx !== null && my !== null) {
|
||||||
|
// if middle point exists, transfer p2 -> p1 and p1 -> mp
|
||||||
|
x2 = x1;
|
||||||
|
y2 = y1;
|
||||||
|
x1 = mx;
|
||||||
|
y1 = my;
|
||||||
|
|
||||||
|
// 'remove' middle point
|
||||||
|
mx = null;
|
||||||
|
my = null;
|
||||||
|
|
||||||
|
// subtract pointsize from i to have current point p1 handled again
|
||||||
|
i -= ps;
|
||||||
|
} else if (y1 !== y2 && x1 !== x2) {
|
||||||
|
// create a middle point
|
||||||
|
y2 = y1;
|
||||||
|
mx = x2;
|
||||||
|
my = y1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip x values
|
||||||
|
|
||||||
|
// clip with xmin
|
||||||
|
if (x1 <= x2 && x1 < axisx.min) {
|
||||||
|
if (x2 < axisx.min) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x1 = axisx.min;
|
||||||
|
} else if (x2 <= x1 && x2 < axisx.min) {
|
||||||
|
if (x1 < axisx.min) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x2 = axisx.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip with xmax
|
||||||
|
if (x1 >= x2 && x1 > axisx.max) {
|
||||||
|
if (x2 > axisx.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x1 = axisx.max;
|
||||||
|
} else if (x2 >= x1 && x2 > axisx.max) {
|
||||||
|
if (x1 > axisx.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
|
||||||
|
x2 = axisx.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!areaOpen) {
|
||||||
|
// open area
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
|
||||||
|
areaOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now first check the case where both is outside
|
||||||
|
if (y1 >= axisy.max && y2 >= axisy.max) {
|
||||||
|
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
|
||||||
|
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
|
||||||
|
continue;
|
||||||
|
} else if (y1 <= axisy.min && y2 <= axisy.min) {
|
||||||
|
ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
|
||||||
|
ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// else it's a bit more complicated, there might
|
||||||
|
// be a flat maxed out rectangle first, then a
|
||||||
|
// triangular cutout or reverse; to find these
|
||||||
|
// keep track of the current x values
|
||||||
|
var x1old = x1,
|
||||||
|
x2old = x2;
|
||||||
|
|
||||||
|
// clip the y values, without shortcutting, we
|
||||||
|
// go through all cases in turn
|
||||||
|
|
||||||
|
// clip with ymin
|
||||||
|
if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
|
||||||
|
x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y1 = axisy.min;
|
||||||
|
} else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
|
||||||
|
x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y2 = axisy.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip with ymax
|
||||||
|
if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
|
||||||
|
x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y1 = axisy.max;
|
||||||
|
} else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
|
||||||
|
x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
|
||||||
|
y2 = axisy.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the x value was changed we got a rectangle
|
||||||
|
// to fill
|
||||||
|
if (x1 !== x1old) {
|
||||||
|
ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
|
||||||
|
// it goes to (x1, y1), but we fill that below
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill triangular section, this sometimes result
|
||||||
|
// in redundant points if (x1, y1) hasn't changed
|
||||||
|
// from previous line to, but we just ignore that
|
||||||
|
ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
|
||||||
|
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
|
||||||
|
|
||||||
|
// fill the other rectangle if it's there
|
||||||
|
if (x2 !== x2old) {
|
||||||
|
ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
|
||||||
|
ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
|
||||||
|
|
||||||
|
This function is used for drawing lines or area fill. In case the series has line decimation function
|
||||||
|
attached, before starting to draw, as an optimization the points will first be decimated.
|
||||||
|
|
||||||
|
The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
|
||||||
|
plotHeight are the corresponding parameters of flot used to determine the drawing surface.
|
||||||
|
The function getColorOrGradient is used to compute the fill style of lines and area.
|
||||||
|
*/
|
||||||
|
function drawSeriesLines(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
|
if (series.lines.dashes && ctx.setLineDash) {
|
||||||
|
ctx.setLineDash(series.lines.dashes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var datapoints = {
|
||||||
|
format: series.datapoints.format,
|
||||||
|
points: series.datapoints.points,
|
||||||
|
pointsize: series.datapoints.pointsize
|
||||||
|
};
|
||||||
|
|
||||||
|
if (series.decimate) {
|
||||||
|
datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lw = series.lines.lineWidth;
|
||||||
|
|
||||||
|
ctx.lineWidth = lw;
|
||||||
|
ctx.strokeStyle = series.color;
|
||||||
|
var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight, getColorOrGradient);
|
||||||
|
if (fillStyle) {
|
||||||
|
ctx.fillStyle = fillStyle;
|
||||||
|
plotLineArea(datapoints, series.xaxis, series.yaxis, series.lines.fillTowards || 0, ctx, series.lines.steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lw > 0) {
|
||||||
|
plotLine(datapoints, 0, 0, series.xaxis, series.yaxis, ctx, series.lines.steps);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
|
||||||
|
|
||||||
|
This function is used for drawing points using a given symbol. In case the series has points decimation
|
||||||
|
function attached, before starting to draw, as an optimization the points will first be decimated.
|
||||||
|
|
||||||
|
The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
|
||||||
|
plotHeight are the corresponding parameters of flot used to determine the drawing surface.
|
||||||
|
The function drawSymbol is used to compute and draw the symbol chosen for the points.
|
||||||
|
*/
|
||||||
|
function drawSeriesPoints(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
|
||||||
|
function drawCircle(ctx, x, y, radius, shadow, fill) {
|
||||||
|
ctx.moveTo(x + radius, y);
|
||||||
|
ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
|
||||||
|
}
|
||||||
|
drawCircle.fill = true;
|
||||||
|
function plotPoints(datapoints, radius, fill, offset, shadow, axisx, axisy, drawSymbolFn) {
|
||||||
|
var points = datapoints.points,
|
||||||
|
ps = datapoints.pointsize;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
for (var i = 0; i < points.length; i += ps) {
|
||||||
|
var x = points[i],
|
||||||
|
y = points[i + 1];
|
||||||
|
if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
x = axisx.p2c(x);
|
||||||
|
y = axisy.p2c(y) + offset;
|
||||||
|
|
||||||
|
drawSymbolFn(ctx, x, y, radius, shadow, fill);
|
||||||
|
}
|
||||||
|
if (drawSymbolFn.fill && !shadow) {
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
|
||||||
|
var datapoints = {
|
||||||
|
format: series.datapoints.format,
|
||||||
|
points: series.datapoints.points,
|
||||||
|
pointsize: series.datapoints.pointsize
|
||||||
|
};
|
||||||
|
|
||||||
|
if (series.decimatePoints) {
|
||||||
|
datapoints.points = series.decimatePoints(series, series.xaxis.min, series.xaxis.max, plotWidth, series.yaxis.min, series.yaxis.max, plotHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lw = series.points.lineWidth,
|
||||||
|
radius = series.points.radius,
|
||||||
|
symbol = series.points.symbol,
|
||||||
|
drawSymbolFn;
|
||||||
|
|
||||||
|
if (symbol === 'circle') {
|
||||||
|
drawSymbolFn = drawCircle;
|
||||||
|
} else if (typeof symbol === 'string' && drawSymbol && drawSymbol[symbol]) {
|
||||||
|
drawSymbolFn = drawSymbol[symbol];
|
||||||
|
} else if (typeof drawSymbol === 'function') {
|
||||||
|
drawSymbolFn = drawSymbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user sets the line width to 0, we change it to a very
|
||||||
|
// small value. A line width of 0 seems to force the default of 1.
|
||||||
|
|
||||||
|
if (lw === 0) {
|
||||||
|
lw = 0.0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.lineWidth = lw;
|
||||||
|
ctx.fillStyle = getFillStyle(series.points, series.color, null, null, getColorOrGradient);
|
||||||
|
ctx.strokeStyle = series.color;
|
||||||
|
plotPoints(datapoints, radius,
|
||||||
|
true, 0, false,
|
||||||
|
series.xaxis, series.yaxis, drawSymbolFn);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBar(x, y, b, barLeft, barRight, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
|
||||||
|
var left = x + barLeft,
|
||||||
|
right = x + barRight,
|
||||||
|
bottom = b, top = y,
|
||||||
|
drawLeft, drawRight, drawTop, drawBottom = false,
|
||||||
|
tmp;
|
||||||
|
|
||||||
|
drawLeft = drawRight = drawTop = true;
|
||||||
|
|
||||||
|
// in horizontal mode, we start the bar from the left
|
||||||
|
// instead of from the bottom so it appears to be
|
||||||
|
// horizontal rather than vertical
|
||||||
|
if (horizontal) {
|
||||||
|
drawBottom = drawRight = drawTop = true;
|
||||||
|
drawLeft = false;
|
||||||
|
left = b;
|
||||||
|
right = x;
|
||||||
|
top = y + barLeft;
|
||||||
|
bottom = y + barRight;
|
||||||
|
|
||||||
|
// account for negative bars
|
||||||
|
if (right < left) {
|
||||||
|
tmp = right;
|
||||||
|
right = left;
|
||||||
|
left = tmp;
|
||||||
|
drawLeft = true;
|
||||||
|
drawRight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
drawLeft = drawRight = drawTop = true;
|
||||||
|
drawBottom = false;
|
||||||
|
left = x + barLeft;
|
||||||
|
right = x + barRight;
|
||||||
|
bottom = b;
|
||||||
|
top = y;
|
||||||
|
|
||||||
|
// account for negative bars
|
||||||
|
if (top < bottom) {
|
||||||
|
tmp = top;
|
||||||
|
top = bottom;
|
||||||
|
bottom = tmp;
|
||||||
|
drawBottom = true;
|
||||||
|
drawTop = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip
|
||||||
|
if (right < axisx.min || left > axisx.max ||
|
||||||
|
top < axisy.min || bottom > axisy.max) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (left < axisx.min) {
|
||||||
|
left = axisx.min;
|
||||||
|
drawLeft = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right > axisx.max) {
|
||||||
|
right = axisx.max;
|
||||||
|
drawRight = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottom < axisy.min) {
|
||||||
|
bottom = axisy.min;
|
||||||
|
drawBottom = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (top > axisy.max) {
|
||||||
|
top = axisy.max;
|
||||||
|
drawTop = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
left = axisx.p2c(left);
|
||||||
|
bottom = axisy.p2c(bottom);
|
||||||
|
right = axisx.p2c(right);
|
||||||
|
top = axisy.p2c(top);
|
||||||
|
|
||||||
|
// fill the bar
|
||||||
|
if (fillStyleCallback) {
|
||||||
|
c.fillStyle = fillStyleCallback(bottom, top);
|
||||||
|
c.fillRect(left, top, right - left, bottom - top)
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw outline
|
||||||
|
if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
|
||||||
|
c.beginPath();
|
||||||
|
|
||||||
|
// FIXME: inline moveTo is buggy with excanvas
|
||||||
|
c.moveTo(left, bottom);
|
||||||
|
if (drawLeft) {
|
||||||
|
c.lineTo(left, top);
|
||||||
|
} else {
|
||||||
|
c.moveTo(left, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawTop) {
|
||||||
|
c.lineTo(right, top);
|
||||||
|
} else {
|
||||||
|
c.moveTo(right, top);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawRight) {
|
||||||
|
c.lineTo(right, bottom);
|
||||||
|
} else {
|
||||||
|
c.moveTo(right, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (drawBottom) {
|
||||||
|
c.lineTo(left, bottom);
|
||||||
|
} else {
|
||||||
|
c.moveTo(left, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient)
|
||||||
|
|
||||||
|
This function is used for drawing series represented as bars. In case the series has decimation
|
||||||
|
function attached, before starting to draw, as an optimization the points will first be decimated.
|
||||||
|
|
||||||
|
The series parameter contains the series to be drawn on ctx context. The plotOffset, plotWidth and
|
||||||
|
plotHeight are the corresponding parameters of flot used to determine the drawing surface.
|
||||||
|
The function getColorOrGradient is used to compute the fill style of bars.
|
||||||
|
*/
|
||||||
|
function drawSeriesBars(series, ctx, plotOffset, plotWidth, plotHeight, drawSymbol, getColorOrGradient) {
|
||||||
|
function plotBars(datapoints, barLeft, barRight, fillStyleCallback, axisx, axisy) {
|
||||||
|
var points = datapoints.points,
|
||||||
|
ps = datapoints.pointsize,
|
||||||
|
fillTowards = series.bars.fillTowards || 0,
|
||||||
|
defaultBottom = fillTowards > axisy.min ? Math.min(axisy.max, fillTowards) : axisy.min;
|
||||||
|
|
||||||
|
for (var i = 0; i < points.length; i += ps) {
|
||||||
|
if (points[i] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use third point as bottom if pointsize is 3
|
||||||
|
var bottom = ps === 3 ? points[i + 2] : defaultBottom;
|
||||||
|
drawBar(points[i], points[i + 1], bottom, barLeft, barRight, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
|
||||||
|
var datapoints = {
|
||||||
|
format: series.datapoints.format,
|
||||||
|
points: series.datapoints.points,
|
||||||
|
pointsize: series.datapoints.pointsize
|
||||||
|
};
|
||||||
|
|
||||||
|
if (series.decimate) {
|
||||||
|
datapoints.points = series.decimate(series, series.xaxis.min, series.xaxis.max, plotWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.lineWidth = series.bars.lineWidth;
|
||||||
|
ctx.strokeStyle = series.color;
|
||||||
|
|
||||||
|
var barLeft;
|
||||||
|
var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
|
||||||
|
switch (series.bars.align) {
|
||||||
|
case "left":
|
||||||
|
barLeft = 0;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
barLeft = -barWidth;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
barLeft = -barWidth / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var fillStyleCallback = series.bars.fill ? function(bottom, top) {
|
||||||
|
return getFillStyle(series.bars, series.color, bottom, top, getColorOrGradient);
|
||||||
|
} : null;
|
||||||
|
|
||||||
|
plotBars(datapoints, barLeft, barLeft + barWidth, fillStyleCallback, series.xaxis, series.yaxis);
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFillStyle(filloptions, seriesColor, bottom, top, getColorOrGradient) {
|
||||||
|
var fill = filloptions.fill;
|
||||||
|
if (!fill) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filloptions.fillColor) {
|
||||||
|
return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = $.color.parse(seriesColor);
|
||||||
|
c.a = typeof fill === "number" ? fill : 0.4;
|
||||||
|
c.normalize();
|
||||||
|
return c.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawSeriesLines = drawSeriesLines;
|
||||||
|
this.drawSeriesPoints = drawSeriesPoints;
|
||||||
|
this.drawSeriesBars = drawSeriesBars;
|
||||||
|
this.drawBar = drawBar;
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.drawSeries = new DrawSeries();
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
/* Flot plugin for plotting error bars.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
Error bars are used to show standard deviation and other statistical
|
||||||
|
properties in a plot.
|
||||||
|
|
||||||
|
* Created by Rui Pereira - rui (dot) pereira (at) gmail (dot) com
|
||||||
|
|
||||||
|
This plugin allows you to plot error-bars over points. Set "errorbars" inside
|
||||||
|
the points series to the axis name over which there will be error values in
|
||||||
|
your data array (*even* if you do not intend to plot them later, by setting
|
||||||
|
"show: null" on xerr/yerr).
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
|
||||||
|
series: {
|
||||||
|
points: {
|
||||||
|
errorbars: "x" or "y" or "xy",
|
||||||
|
xerr: {
|
||||||
|
show: null/false or true,
|
||||||
|
asymmetric: null/false or true,
|
||||||
|
upperCap: null or "-" or function,
|
||||||
|
lowerCap: null or "-" or function,
|
||||||
|
color: null or color,
|
||||||
|
radius: null or number
|
||||||
|
},
|
||||||
|
yerr: { same options as xerr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Each data point array is expected to be of the type:
|
||||||
|
|
||||||
|
"x" [ x, y, xerr ]
|
||||||
|
"y" [ x, y, yerr ]
|
||||||
|
"xy" [ x, y, xerr, yerr ]
|
||||||
|
|
||||||
|
Where xerr becomes xerr_lower,xerr_upper for the asymmetric error case, and
|
||||||
|
equivalently for yerr. Eg., a datapoint for the "xy" case with symmetric
|
||||||
|
error-bars on X and asymmetric on Y would be:
|
||||||
|
|
||||||
|
[ x, y, xerr, yerr_lower, yerr_upper ]
|
||||||
|
|
||||||
|
By default no end caps are drawn. Setting upperCap and/or lowerCap to "-" will
|
||||||
|
draw a small cap perpendicular to the error bar. They can also be set to a
|
||||||
|
user-defined drawing function, with (ctx, x, y, radius) as parameters, as eg.
|
||||||
|
|
||||||
|
function drawSemiCircle( ctx, x, y, radius ) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc( x, y, radius, 0, Math.PI, false );
|
||||||
|
ctx.moveTo( x - radius, y );
|
||||||
|
ctx.lineTo( x + radius, y );
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color and radius both default to the same ones of the points series if not
|
||||||
|
set. The independent radius parameter on xerr/yerr is useful for the case when
|
||||||
|
we may want to add error-bars to a line, without showing the interconnecting
|
||||||
|
points (with radius: 0), and still showing end caps on the error-bars.
|
||||||
|
shadowSize and lineWidth are derived as well from the points series.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: {
|
||||||
|
points: {
|
||||||
|
errorbars: null, //should be 'x', 'y' or 'xy'
|
||||||
|
xerr: {err: 'x', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null},
|
||||||
|
yerr: {err: 'y', show: null, asymmetric: null, upperCap: null, lowerCap: null, color: null, radius: null}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function processRawData(plot, series, data, datapoints) {
|
||||||
|
if (!series.points.errorbars) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// x,y values
|
||||||
|
var format = [
|
||||||
|
{ x: true, number: true, required: true },
|
||||||
|
{ y: true, number: true, required: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
var errors = series.points.errorbars;
|
||||||
|
// error bars - first X then Y
|
||||||
|
if (errors === 'x' || errors === 'xy') {
|
||||||
|
// lower / upper error
|
||||||
|
if (series.points.xerr.asymmetric) {
|
||||||
|
format.push({ x: true, number: true, required: true });
|
||||||
|
format.push({ x: true, number: true, required: true });
|
||||||
|
} else {
|
||||||
|
format.push({ x: true, number: true, required: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors === 'y' || errors === 'xy') {
|
||||||
|
// lower / upper error
|
||||||
|
if (series.points.yerr.asymmetric) {
|
||||||
|
format.push({ y: true, number: true, required: true });
|
||||||
|
format.push({ y: true, number: true, required: true });
|
||||||
|
} else {
|
||||||
|
format.push({ y: true, number: true, required: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
datapoints.format = format;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseErrors(series, i) {
|
||||||
|
var points = series.datapoints.points;
|
||||||
|
|
||||||
|
// read errors from points array
|
||||||
|
var exl = null,
|
||||||
|
exu = null,
|
||||||
|
eyl = null,
|
||||||
|
eyu = null;
|
||||||
|
var xerr = series.points.xerr,
|
||||||
|
yerr = series.points.yerr;
|
||||||
|
|
||||||
|
var eb = series.points.errorbars;
|
||||||
|
// error bars - first X
|
||||||
|
if (eb === 'x' || eb === 'xy') {
|
||||||
|
if (xerr.asymmetric) {
|
||||||
|
exl = points[i + 2];
|
||||||
|
exu = points[i + 3];
|
||||||
|
if (eb === 'xy') {
|
||||||
|
if (yerr.asymmetric) {
|
||||||
|
eyl = points[i + 4];
|
||||||
|
eyu = points[i + 5];
|
||||||
|
} else {
|
||||||
|
eyl = points[i + 4];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
exl = points[i + 2];
|
||||||
|
if (eb === 'xy') {
|
||||||
|
if (yerr.asymmetric) {
|
||||||
|
eyl = points[i + 3];
|
||||||
|
eyu = points[i + 4];
|
||||||
|
} else {
|
||||||
|
eyl = points[i + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// only Y
|
||||||
|
} else {
|
||||||
|
if (eb === 'y') {
|
||||||
|
if (yerr.asymmetric) {
|
||||||
|
eyl = points[i + 2];
|
||||||
|
eyu = points[i + 3];
|
||||||
|
} else {
|
||||||
|
eyl = points[i + 2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// symmetric errors?
|
||||||
|
if (exu == null) exu = exl;
|
||||||
|
if (eyu == null) eyu = eyl;
|
||||||
|
|
||||||
|
var errRanges = [exl, exu, eyl, eyu];
|
||||||
|
// nullify if not showing
|
||||||
|
if (!xerr.show) {
|
||||||
|
errRanges[0] = null;
|
||||||
|
errRanges[1] = null;
|
||||||
|
}
|
||||||
|
if (!yerr.show) {
|
||||||
|
errRanges[2] = null;
|
||||||
|
errRanges[3] = null;
|
||||||
|
}
|
||||||
|
return errRanges;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSeriesErrors(plot, ctx, s) {
|
||||||
|
var points = s.datapoints.points,
|
||||||
|
ps = s.datapoints.pointsize,
|
||||||
|
ax = [s.xaxis, s.yaxis],
|
||||||
|
radius = s.points.radius,
|
||||||
|
err = [s.points.xerr, s.points.yerr],
|
||||||
|
tmp;
|
||||||
|
|
||||||
|
//sanity check, in case some inverted axis hack is applied to flot
|
||||||
|
var invertX = false;
|
||||||
|
if (ax[0].p2c(ax[0].max) < ax[0].p2c(ax[0].min)) {
|
||||||
|
invertX = true;
|
||||||
|
tmp = err[0].lowerCap;
|
||||||
|
err[0].lowerCap = err[0].upperCap;
|
||||||
|
err[0].upperCap = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invertY = false;
|
||||||
|
if (ax[1].p2c(ax[1].min) < ax[1].p2c(ax[1].max)) {
|
||||||
|
invertY = true;
|
||||||
|
tmp = err[1].lowerCap;
|
||||||
|
err[1].lowerCap = err[1].upperCap;
|
||||||
|
err[1].upperCap = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < s.datapoints.points.length; i += ps) {
|
||||||
|
//parse
|
||||||
|
var errRanges = parseErrors(s, i);
|
||||||
|
|
||||||
|
//cycle xerr & yerr
|
||||||
|
for (var e = 0; e < err.length; e++) {
|
||||||
|
var minmax = [ax[e].min, ax[e].max];
|
||||||
|
|
||||||
|
//draw this error?
|
||||||
|
if (errRanges[e * err.length]) {
|
||||||
|
//data coordinates
|
||||||
|
var x = points[i],
|
||||||
|
y = points[i + 1];
|
||||||
|
|
||||||
|
//errorbar ranges
|
||||||
|
var upper = [x, y][e] + errRanges[e * err.length + 1],
|
||||||
|
lower = [x, y][e] - errRanges[e * err.length];
|
||||||
|
|
||||||
|
//points outside of the canvas
|
||||||
|
if (err[e].err === 'x') {
|
||||||
|
if (y > ax[1].max || y < ax[1].min || upper < ax[0].min || lower > ax[0].max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err[e].err === 'y') {
|
||||||
|
if (x > ax[0].max || x < ax[0].min || upper < ax[1].min || lower > ax[1].max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prevent errorbars getting out of the canvas
|
||||||
|
var drawUpper = true,
|
||||||
|
drawLower = true;
|
||||||
|
|
||||||
|
if (upper > minmax[1]) {
|
||||||
|
drawUpper = false;
|
||||||
|
upper = minmax[1];
|
||||||
|
}
|
||||||
|
if (lower < minmax[0]) {
|
||||||
|
drawLower = false;
|
||||||
|
lower = minmax[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
//sanity check, in case some inverted axis hack is applied to flot
|
||||||
|
if ((err[e].err === 'x' && invertX) || (err[e].err === 'y' && invertY)) {
|
||||||
|
//swap coordinates
|
||||||
|
tmp = lower;
|
||||||
|
lower = upper;
|
||||||
|
upper = tmp;
|
||||||
|
tmp = drawLower;
|
||||||
|
drawLower = drawUpper;
|
||||||
|
drawUpper = tmp;
|
||||||
|
tmp = minmax[0];
|
||||||
|
minmax[0] = minmax[1];
|
||||||
|
minmax[1] = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert to pixels
|
||||||
|
x = ax[0].p2c(x);
|
||||||
|
y = ax[1].p2c(y);
|
||||||
|
upper = ax[e].p2c(upper);
|
||||||
|
lower = ax[e].p2c(lower);
|
||||||
|
minmax[0] = ax[e].p2c(minmax[0]);
|
||||||
|
minmax[1] = ax[e].p2c(minmax[1]);
|
||||||
|
|
||||||
|
//same style as points by default
|
||||||
|
var lw = err[e].lineWidth ? err[e].lineWidth : s.points.lineWidth,
|
||||||
|
sw = s.points.shadowSize != null ? s.points.shadowSize : s.shadowSize;
|
||||||
|
|
||||||
|
//shadow as for points
|
||||||
|
if (lw > 0 && sw > 0) {
|
||||||
|
var w = sw / 2;
|
||||||
|
ctx.lineWidth = w;
|
||||||
|
ctx.strokeStyle = "rgba(0,0,0,0.1)";
|
||||||
|
drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w + w / 2, minmax);
|
||||||
|
|
||||||
|
ctx.strokeStyle = "rgba(0,0,0,0.2)";
|
||||||
|
drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, w / 2, minmax);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = err[e].color
|
||||||
|
? err[e].color
|
||||||
|
: s.color;
|
||||||
|
ctx.lineWidth = lw;
|
||||||
|
//draw it
|
||||||
|
drawError(ctx, err[e], x, y, upper, lower, drawUpper, drawLower, radius, 0, minmax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawError(ctx, err, x, y, upper, lower, drawUpper, drawLower, radius, offset, minmax) {
|
||||||
|
//shadow offset
|
||||||
|
y += offset;
|
||||||
|
upper += offset;
|
||||||
|
lower += offset;
|
||||||
|
|
||||||
|
// error bar - avoid plotting over circles
|
||||||
|
if (err.err === 'x') {
|
||||||
|
if (upper > x + radius) drawPath(ctx, [[upper, y], [Math.max(x + radius, minmax[0]), y]]);
|
||||||
|
else drawUpper = false;
|
||||||
|
|
||||||
|
if (lower < x - radius) drawPath(ctx, [[Math.min(x - radius, minmax[1]), y], [lower, y]]);
|
||||||
|
else drawLower = false;
|
||||||
|
} else {
|
||||||
|
if (upper < y - radius) drawPath(ctx, [[x, upper], [x, Math.min(y - radius, minmax[0])]]);
|
||||||
|
else drawUpper = false;
|
||||||
|
|
||||||
|
if (lower > y + radius) drawPath(ctx, [[x, Math.max(y + radius, minmax[1])], [x, lower]]);
|
||||||
|
else drawLower = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//internal radius value in errorbar, allows to plot radius 0 points and still keep proper sized caps
|
||||||
|
//this is a way to get errorbars on lines without visible connecting dots
|
||||||
|
radius = err.radius != null
|
||||||
|
? err.radius
|
||||||
|
: radius;
|
||||||
|
|
||||||
|
// upper cap
|
||||||
|
if (drawUpper) {
|
||||||
|
if (err.upperCap === '-') {
|
||||||
|
if (err.err === 'x') drawPath(ctx, [[upper, y - radius], [upper, y + radius]]);
|
||||||
|
else drawPath(ctx, [[x - radius, upper], [x + radius, upper]]);
|
||||||
|
} else if ($.isFunction(err.upperCap)) {
|
||||||
|
if (err.err === 'x') err.upperCap(ctx, upper, y, radius);
|
||||||
|
else err.upperCap(ctx, x, upper, radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// lower cap
|
||||||
|
if (drawLower) {
|
||||||
|
if (err.lowerCap === '-') {
|
||||||
|
if (err.err === 'x') drawPath(ctx, [[lower, y - radius], [lower, y + radius]]);
|
||||||
|
else drawPath(ctx, [[x - radius, lower], [x + radius, lower]]);
|
||||||
|
} else if ($.isFunction(err.lowerCap)) {
|
||||||
|
if (err.err === 'x') err.lowerCap(ctx, lower, y, radius);
|
||||||
|
else err.lowerCap(ctx, x, lower, radius);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPath(ctx, pts) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pts[0][0], pts[0][1]);
|
||||||
|
for (var p = 1; p < pts.length; p++) {
|
||||||
|
ctx.lineTo(pts[p][0], pts[p][1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(plot, ctx) {
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
$.each(plot.getData(), function (i, s) {
|
||||||
|
if (s.points.errorbars && (s.points.xerr.show || s.points.yerr.show)) {
|
||||||
|
drawSeriesErrors(plot, ctx, s);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processRawData.push(processRawData);
|
||||||
|
plot.hooks.draw.push(draw);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'errorbars',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/* Flot plugin for computing bottoms for filled line and bar charts.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The case: you've got two series that you want to fill the area between. In Flot
|
||||||
|
terms, you need to use one as the fill bottom of the other. You can specify the
|
||||||
|
bottom of each data point as the third coordinate manually, or you can use this
|
||||||
|
plugin to compute it for you.
|
||||||
|
|
||||||
|
In order to name the other series, you need to give it an id, like this:
|
||||||
|
|
||||||
|
var dataset = [
|
||||||
|
{ data: [ ... ], id: "foo" } , // use default bottom
|
||||||
|
{ data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom
|
||||||
|
];
|
||||||
|
|
||||||
|
$.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }});
|
||||||
|
|
||||||
|
As a convenience, if the id given is a number that doesn't appear as an id in
|
||||||
|
the series, it is interpreted as the index in the array instead (so fillBetween:
|
||||||
|
0 can also mean the first series).
|
||||||
|
|
||||||
|
Internally, the plugin modifies the datapoints in each series. For line series,
|
||||||
|
extra data points might be inserted through interpolation. Note that at points
|
||||||
|
where the bottom line is not defined (due to a null point or start/end of line),
|
||||||
|
the current line will show a gap too. The algorithm comes from the
|
||||||
|
jquery.flot.stack.js plugin, possibly some code could be shared.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: {
|
||||||
|
fillBetween: null // or number
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function findBottomSeries(s, allseries) {
|
||||||
|
var i;
|
||||||
|
|
||||||
|
for (i = 0; i < allseries.length; ++i) {
|
||||||
|
if (allseries[ i ].id === s.fillBetween) {
|
||||||
|
return allseries[ i ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof s.fillBetween === "number") {
|
||||||
|
if (s.fillBetween < 0 || s.fillBetween >= allseries.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return allseries[ s.fillBetween ];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeFormat(plot, s, data, datapoints) {
|
||||||
|
if (s.fillBetween == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
format = datapoints.format;
|
||||||
|
var plotHasId = function(id) {
|
||||||
|
var plotData = plot.getData();
|
||||||
|
for (i = 0; i < plotData.length; i++) {
|
||||||
|
if (plotData[i].id === id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!format) {
|
||||||
|
format = [];
|
||||||
|
|
||||||
|
format.push({
|
||||||
|
x: true,
|
||||||
|
number: true,
|
||||||
|
computeRange: s.xaxis.options.autoScale !== 'none',
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
format.push({
|
||||||
|
y: true,
|
||||||
|
number: true,
|
||||||
|
computeRange: s.yaxis.options.autoScale !== 'none',
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (s.fillBetween !== undefined && s.fillBetween !== '' && plotHasId(s.fillBetween) && s.fillBetween !== s.id) {
|
||||||
|
format.push({
|
||||||
|
x: false,
|
||||||
|
y: true,
|
||||||
|
number: true,
|
||||||
|
required: false,
|
||||||
|
computeRange: s.yaxis.options.autoScale !== 'none',
|
||||||
|
defaultValue: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.format = format;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeFillBottoms(plot, s, datapoints) {
|
||||||
|
if (s.fillBetween == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var other = findBottomSeries(s, plot.getData());
|
||||||
|
|
||||||
|
if (!other) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ps = datapoints.pointsize,
|
||||||
|
points = datapoints.points,
|
||||||
|
otherps = other.datapoints.pointsize,
|
||||||
|
otherpoints = other.datapoints.points,
|
||||||
|
newpoints = [],
|
||||||
|
px, py, intery, qx, qy, bottom,
|
||||||
|
withlines = s.lines.show,
|
||||||
|
withbottom = ps > 2 && datapoints.format[2].y,
|
||||||
|
withsteps = withlines && s.lines.steps,
|
||||||
|
fromgap = true,
|
||||||
|
i = 0,
|
||||||
|
j = 0,
|
||||||
|
l, m;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (i >= points.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
l = newpoints.length;
|
||||||
|
|
||||||
|
if (points[ i ] == null) {
|
||||||
|
// copy gaps
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[ i + m ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
} else if (j >= otherpoints.length) {
|
||||||
|
// for lines, we can't use the rest of the points
|
||||||
|
if (!withlines) {
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[ i + m ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
} else if (otherpoints[ j ] == null) {
|
||||||
|
// oops, got a gap
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fromgap = true;
|
||||||
|
j += otherps;
|
||||||
|
} else {
|
||||||
|
// cases where we actually got two points
|
||||||
|
px = points[ i ];
|
||||||
|
py = points[ i + 1 ];
|
||||||
|
qx = otherpoints[ j ];
|
||||||
|
qy = otherpoints[ j + 1 ];
|
||||||
|
bottom = 0;
|
||||||
|
|
||||||
|
if (px === qx) {
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[ i + m ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
//newpoints[ l + 1 ] += qy;
|
||||||
|
bottom = qy;
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
j += otherps;
|
||||||
|
} else if (px > qx) {
|
||||||
|
// we got past point below, might need to
|
||||||
|
// insert interpolated extra point
|
||||||
|
|
||||||
|
if (withlines && i > 0 && points[ i - ps ] != null) {
|
||||||
|
intery = py + (points[ i - ps + 1 ] - py) * (qx - px) / (points[ i - ps ] - px);
|
||||||
|
newpoints.push(qx);
|
||||||
|
newpoints.push(intery);
|
||||||
|
for (m = 2; m < ps; ++m) {
|
||||||
|
newpoints.push(points[ i + m ]);
|
||||||
|
}
|
||||||
|
bottom = qy;
|
||||||
|
}
|
||||||
|
|
||||||
|
j += otherps;
|
||||||
|
} else {
|
||||||
|
// px < qx
|
||||||
|
// if we come from a gap, we just skip this point
|
||||||
|
|
||||||
|
if (fromgap && withlines) {
|
||||||
|
i += ps;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[ i + m ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we might be able to interpolate a point below,
|
||||||
|
// this can give us a better y
|
||||||
|
|
||||||
|
if (withlines && j > 0 && otherpoints[ j - otherps ] != null) {
|
||||||
|
bottom = qy + (otherpoints[ j - otherps + 1 ] - qy) * (px - qx) / (otherpoints[ j - otherps ] - qx);
|
||||||
|
}
|
||||||
|
|
||||||
|
//newpoints[l + 1] += bottom;
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromgap = false;
|
||||||
|
|
||||||
|
if (l !== newpoints.length && withbottom) {
|
||||||
|
newpoints[ l + 2 ] = bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maintain the line steps invariant
|
||||||
|
|
||||||
|
if (withsteps && l !== newpoints.length && l > 0 &&
|
||||||
|
newpoints[ l ] !== null &&
|
||||||
|
newpoints[ l ] !== newpoints[ l - ps ] &&
|
||||||
|
newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ]) {
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints[ l + ps + m ] = newpoints[ l + m ];
|
||||||
|
}
|
||||||
|
newpoints[ l + 1 ] = newpoints[ l - ps + 1 ];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.points = newpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.processRawData.push(computeFormat);
|
||||||
|
plot.hooks.processDatapoints.push(computeFillBottoms);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: "fillbetween",
|
||||||
|
version: "1.0"
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/* Support for flat 1D data series.
|
||||||
|
|
||||||
|
A 1D flat data series is a data series in the form of a regular 1D array. The
|
||||||
|
main reason for using a flat data series is that it performs better, consumes
|
||||||
|
less memory and generates less garbage collection than the regular flot format.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
plot.setData([[[0,0], [1,1], [2,2], [3,3]]]); // regular flot format
|
||||||
|
plot.setData([{flatdata: true, data: [0, 1, 2, 3]}]); // flatdata format
|
||||||
|
|
||||||
|
Set series.flatdata to true to enable this plugin.
|
||||||
|
|
||||||
|
You can use series.start to specify the starting index of the series (default is 0)
|
||||||
|
You can use series.step to specify the interval between consecutive indexes of the series (default is 1)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global jQuery*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
function process1DRawData(plot, series, data, datapoints) {
|
||||||
|
if (series.flatdata === true) {
|
||||||
|
var start = series.start || 0;
|
||||||
|
var step = typeof series.step === 'number' ? series.step : 1;
|
||||||
|
datapoints.pointsize = 2;
|
||||||
|
for (var i = 0, j = 0; i < data.length; i++, j += 2) {
|
||||||
|
datapoints.points[j] = start + (i * step);
|
||||||
|
datapoints.points[j + 1] = data[i];
|
||||||
|
}
|
||||||
|
if (datapoints.points !== undefined) {
|
||||||
|
datapoints.points.length = data.length * 2;
|
||||||
|
} else {
|
||||||
|
datapoints.points = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: function(plot) {
|
||||||
|
plot.hooks.processRawData.push(process1DRawData);
|
||||||
|
},
|
||||||
|
name: 'flatdata',
|
||||||
|
version: '0.0.2'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,350 @@
|
|||||||
|
/* global jQuery */
|
||||||
|
|
||||||
|
/**
|
||||||
|
## jquery.flot.hover.js
|
||||||
|
|
||||||
|
This plugin is used for mouse hover and tap on a point of plot series.
|
||||||
|
It supports the following options:
|
||||||
|
```js
|
||||||
|
grid: {
|
||||||
|
hoverable: false, //to trigger plothover event on mouse hover or tap on a point
|
||||||
|
clickable: false //to trigger plotclick event on mouse hover
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It listens to native mouse move event or click, as well as artificial generated
|
||||||
|
tap and touchevent.
|
||||||
|
|
||||||
|
When the mouse is over a point or a tap on a point is performed, that point or
|
||||||
|
the correscponding bar will be highlighted and a "plothover" event will be generated.
|
||||||
|
|
||||||
|
Custom "touchevent" is triggered when any touch interaction is made. Hover plugin
|
||||||
|
handles this events by unhighlighting all of the previously highlighted points and generates
|
||||||
|
"plothovercleanup" event to notify any part that is handling plothover (for exemple to cleanup
|
||||||
|
the tooltip from webcharts).
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
grid: {
|
||||||
|
hoverable: false,
|
||||||
|
clickable: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var browser = $.plot.browser;
|
||||||
|
|
||||||
|
var eventType = {
|
||||||
|
click: 'click',
|
||||||
|
hover: 'hover'
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
var lastMouseMoveEvent;
|
||||||
|
var highlights = [];
|
||||||
|
|
||||||
|
function bindEvents(plot, eventHolder) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
if (o.grid.hoverable || o.grid.clickable) {
|
||||||
|
eventHolder[0].addEventListener('touchevent', triggerCleanupEvent, false);
|
||||||
|
eventHolder[0].addEventListener('tap', generatePlothoverEvent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.grid.clickable) {
|
||||||
|
eventHolder.bind("click", onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.grid.hoverable) {
|
||||||
|
eventHolder.bind("mousemove", onMouseMove);
|
||||||
|
|
||||||
|
// Use bind, rather than .mouseleave, because we officially
|
||||||
|
// still support jQuery 1.2.6, which doesn't define a shortcut
|
||||||
|
// for mouseenter or mouseleave. This was a bug/oversight that
|
||||||
|
// was fixed somewhere around 1.3.x. We can return to using
|
||||||
|
// .mouseleave when we drop support for 1.2.6.
|
||||||
|
|
||||||
|
eventHolder.bind("mouseleave", onMouseLeave);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(plot, eventHolder) {
|
||||||
|
eventHolder[0].removeEventListener('tap', generatePlothoverEvent);
|
||||||
|
eventHolder[0].removeEventListener('touchevent', triggerCleanupEvent);
|
||||||
|
eventHolder.unbind("mousemove", onMouseMove);
|
||||||
|
eventHolder.unbind("mouseleave", onMouseLeave);
|
||||||
|
eventHolder.unbind("click", onClick);
|
||||||
|
highlights = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function generatePlothoverEvent(e) {
|
||||||
|
var o = plot.getOptions(),
|
||||||
|
newEvent = new CustomEvent('mouseevent');
|
||||||
|
|
||||||
|
//transform from touch event to mouse event format
|
||||||
|
newEvent.pageX = e.detail.changedTouches[0].pageX;
|
||||||
|
newEvent.pageY = e.detail.changedTouches[0].pageY;
|
||||||
|
newEvent.clientX = e.detail.changedTouches[0].clientX;
|
||||||
|
newEvent.clientY = e.detail.changedTouches[0].clientY;
|
||||||
|
|
||||||
|
if (o.grid.hoverable) {
|
||||||
|
doTriggerClickHoverEvent(newEvent, eventType.hover, 30);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function doTriggerClickHoverEvent(event, eventType, searchDistance) {
|
||||||
|
var series = plot.getData();
|
||||||
|
if (event !== undefined
|
||||||
|
&& series.length > 0
|
||||||
|
&& series[0].xaxis.c2p !== undefined
|
||||||
|
&& series[0].yaxis.c2p !== undefined) {
|
||||||
|
var eventToTrigger = "plot" + eventType;
|
||||||
|
var seriesFlag = eventType + "able";
|
||||||
|
triggerClickHoverEvent(eventToTrigger, event,
|
||||||
|
function(i) {
|
||||||
|
return series[i][seriesFlag] !== false;
|
||||||
|
}, searchDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
lastMouseMoveEvent = e;
|
||||||
|
plot.getPlaceholder()[0].lastMouseMoveEvent = e;
|
||||||
|
doTriggerClickHoverEvent(e, eventType.hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseLeave(e) {
|
||||||
|
lastMouseMoveEvent = undefined;
|
||||||
|
plot.getPlaceholder()[0].lastMouseMoveEvent = undefined;
|
||||||
|
triggerClickHoverEvent("plothover", e,
|
||||||
|
function(i) {
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(e) {
|
||||||
|
doTriggerClickHoverEvent(e, eventType.click);
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerCleanupEvent() {
|
||||||
|
plot.unhighlight();
|
||||||
|
plot.getPlaceholder().trigger('plothovercleanup');
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger click or hover event (they send the same parameters
|
||||||
|
// so we share their code)
|
||||||
|
function triggerClickHoverEvent(eventname, event, seriesFilter, searchDistance) {
|
||||||
|
var options = plot.getOptions(),
|
||||||
|
offset = plot.offset(),
|
||||||
|
page = browser.getPageXY(event),
|
||||||
|
canvasX = page.X - offset.left,
|
||||||
|
canvasY = page.Y - offset.top,
|
||||||
|
pos = plot.c2p({
|
||||||
|
left: canvasX,
|
||||||
|
top: canvasY
|
||||||
|
}),
|
||||||
|
distance = searchDistance !== undefined ? searchDistance : options.grid.mouseActiveRadius;
|
||||||
|
|
||||||
|
pos.pageX = page.X;
|
||||||
|
pos.pageY = page.Y;
|
||||||
|
|
||||||
|
var item = plot.findNearbyItem(canvasX, canvasY, seriesFilter, distance);
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
// fill in mouse pos for any listeners out there
|
||||||
|
item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left, 10);
|
||||||
|
item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.grid.autoHighlight) {
|
||||||
|
// clear auto-highlights
|
||||||
|
for (var i = 0; i < highlights.length; ++i) {
|
||||||
|
var h = highlights[i];
|
||||||
|
if ((h.auto === eventname &&
|
||||||
|
!(item && h.series === item.series &&
|
||||||
|
h.point[0] === item.datapoint[0] &&
|
||||||
|
h.point[1] === item.datapoint[1])) || !item) {
|
||||||
|
unhighlight(h.series, h.point);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
highlight(item.series, item.datapoint, eventname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.getPlaceholder().trigger(eventname, [pos, item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlight(s, point, auto) {
|
||||||
|
if (typeof s === "number") {
|
||||||
|
s = plot.getData()[s];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof point === "number") {
|
||||||
|
var ps = s.datapoints.pointsize;
|
||||||
|
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = indexOfHighlight(s, point);
|
||||||
|
if (i === -1) {
|
||||||
|
highlights.push({
|
||||||
|
series: s,
|
||||||
|
point: point,
|
||||||
|
auto: auto
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
} else if (!auto) {
|
||||||
|
highlights[i].auto = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhighlight(s, point) {
|
||||||
|
if (s == null && point == null) {
|
||||||
|
highlights = [];
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof s === "number") {
|
||||||
|
s = plot.getData()[s];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof point === "number") {
|
||||||
|
var ps = s.datapoints.pointsize;
|
||||||
|
point = s.datapoints.points.slice(ps * point, ps * (point + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
var i = indexOfHighlight(s, point);
|
||||||
|
if (i !== -1) {
|
||||||
|
highlights.splice(i, 1);
|
||||||
|
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexOfHighlight(s, p) {
|
||||||
|
for (var i = 0; i < highlights.length; ++i) {
|
||||||
|
var h = highlights[i];
|
||||||
|
if (h.series === s &&
|
||||||
|
h.point[0] === p[0] &&
|
||||||
|
h.point[1] === p[1]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDatapoints() {
|
||||||
|
triggerCleanupEvent();
|
||||||
|
doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupGrid() {
|
||||||
|
doTriggerClickHoverEvent(lastMouseMoveEvent, eventType.hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOverlay(plot, octx, overlay) {
|
||||||
|
var plotOffset = plot.getPlotOffset(),
|
||||||
|
i, hi;
|
||||||
|
|
||||||
|
octx.save();
|
||||||
|
octx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
for (i = 0; i < highlights.length; ++i) {
|
||||||
|
hi = highlights[i];
|
||||||
|
|
||||||
|
if (hi.series.bars.show) drawBarHighlight(hi.series, hi.point, octx);
|
||||||
|
else drawPointHighlight(hi.series, hi.point, octx, plot);
|
||||||
|
}
|
||||||
|
octx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPointHighlight(series, point, octx, plot) {
|
||||||
|
var x = point[0],
|
||||||
|
y = point[1],
|
||||||
|
axisx = series.xaxis,
|
||||||
|
axisy = series.yaxis,
|
||||||
|
highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString();
|
||||||
|
|
||||||
|
if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pointRadius = series.points.radius + series.points.lineWidth / 2;
|
||||||
|
octx.lineWidth = pointRadius;
|
||||||
|
octx.strokeStyle = highlightColor;
|
||||||
|
var radius = 1.5 * pointRadius;
|
||||||
|
x = axisx.p2c(x);
|
||||||
|
y = axisy.p2c(y);
|
||||||
|
|
||||||
|
octx.beginPath();
|
||||||
|
var symbol = series.points.symbol;
|
||||||
|
if (symbol === 'circle') {
|
||||||
|
octx.arc(x, y, radius, 0, 2 * Math.PI, false);
|
||||||
|
} else if (typeof symbol === 'string' && plot.drawSymbol && plot.drawSymbol[symbol]) {
|
||||||
|
plot.drawSymbol[symbol](octx, x, y, radius, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
octx.closePath();
|
||||||
|
octx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBarHighlight(series, point, octx) {
|
||||||
|
var highlightColor = (typeof series.highlightColor === "string") ? series.highlightColor : $.color.parse(series.color).scale('a', 0.5).toString(),
|
||||||
|
fillStyle = highlightColor,
|
||||||
|
barLeft;
|
||||||
|
|
||||||
|
var barWidth = series.bars.barWidth[0] || series.bars.barWidth;
|
||||||
|
switch (series.bars.align) {
|
||||||
|
case "left":
|
||||||
|
barLeft = 0;
|
||||||
|
break;
|
||||||
|
case "right":
|
||||||
|
barLeft = -barWidth;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
barLeft = -barWidth / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
octx.lineWidth = series.bars.lineWidth;
|
||||||
|
octx.strokeStyle = highlightColor;
|
||||||
|
|
||||||
|
var fillTowards = series.bars.fillTowards || 0,
|
||||||
|
bottom = fillTowards > series.yaxis.min ? Math.min(series.yaxis.max, fillTowards) : series.yaxis.min;
|
||||||
|
|
||||||
|
$.plot.drawSeries.drawBar(point[0], point[1], point[2] || bottom, barLeft, barLeft + barWidth,
|
||||||
|
function() {
|
||||||
|
return fillStyle;
|
||||||
|
}, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initHover(plot, options) {
|
||||||
|
plot.highlight = highlight;
|
||||||
|
plot.unhighlight = unhighlight;
|
||||||
|
if (options.grid.hoverable || options.grid.clickable) {
|
||||||
|
plot.hooks.drawOverlay.push(drawOverlay);
|
||||||
|
plot.hooks.processDatapoints.push(processDatapoints);
|
||||||
|
plot.hooks.setupGrid.push(setupGrid);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMouseMoveEvent = plot.getPlaceholder()[0].lastMouseMoveEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(bindEvents);
|
||||||
|
plot.hooks.shutdown.push(shutdown);
|
||||||
|
plot.hooks.processOptions.push(initHover);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'hover',
|
||||||
|
version: '0.1'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
/* Flot plugin for plotting images.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The data syntax is [ [ image, x1, y1, x2, y2 ], ... ] where (x1, y1) and
|
||||||
|
(x2, y2) are where you intend the two opposite corners of the image to end up
|
||||||
|
in the plot. Image must be a fully loaded Javascript image (you can make one
|
||||||
|
with new Image()). If the image is not complete, it's skipped when plotting.
|
||||||
|
|
||||||
|
There are two helpers included for retrieving images. The easiest work the way
|
||||||
|
that you put in URLs instead of images in the data, like this:
|
||||||
|
|
||||||
|
[ "myimage.png", 0, 0, 10, 10 ]
|
||||||
|
|
||||||
|
Then call $.plot.image.loadData( data, options, callback ) where data and
|
||||||
|
options are the same as you pass in to $.plot. This loads the images, replaces
|
||||||
|
the URLs in the data with the corresponding images and calls "callback" when
|
||||||
|
all images are loaded (or failed loading). In the callback, you can then call
|
||||||
|
$.plot with the data set. See the included example.
|
||||||
|
|
||||||
|
A more low-level helper, $.plot.image.load(urls, callback) is also included.
|
||||||
|
Given a list of URLs, it calls callback with an object mapping from URL to
|
||||||
|
Image object when all images are loaded or have failed loading.
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
|
||||||
|
series: {
|
||||||
|
images: {
|
||||||
|
show: boolean
|
||||||
|
anchor: "corner" or "center"
|
||||||
|
alpha: [ 0, 1 ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
They can be specified for a specific series:
|
||||||
|
|
||||||
|
$.plot( $("#placeholder"), [{
|
||||||
|
data: [ ... ],
|
||||||
|
images: { ... }
|
||||||
|
])
|
||||||
|
|
||||||
|
Note that because the data format is different from usual data points, you
|
||||||
|
can't use images with anything else in a specific data series.
|
||||||
|
|
||||||
|
Setting "anchor" to "center" causes the pixels in the image to be anchored at
|
||||||
|
the corner pixel centers inside of at the pixel corners, effectively letting
|
||||||
|
half a pixel stick out to each side in the plot.
|
||||||
|
|
||||||
|
A possible future direction could be support for tiling for large images (like
|
||||||
|
Google Maps).
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: {
|
||||||
|
images: {
|
||||||
|
show: false,
|
||||||
|
alpha: 1,
|
||||||
|
anchor: "corner" // or "center"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.image = {};
|
||||||
|
|
||||||
|
$.plot.image.loadDataImages = function (series, options, callback) {
|
||||||
|
var urls = [], points = [];
|
||||||
|
|
||||||
|
var defaultShow = options.series.images.show;
|
||||||
|
|
||||||
|
$.each(series, function (i, s) {
|
||||||
|
if (!(defaultShow || s.images.show)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.data) {
|
||||||
|
s = s.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each(s, function (i, p) {
|
||||||
|
if (typeof p[0] === "string") {
|
||||||
|
urls.push(p[0]);
|
||||||
|
points.push(p);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$.plot.image.load(urls, function (loadedImages) {
|
||||||
|
$.each(points, function (i, p) {
|
||||||
|
var url = p[0];
|
||||||
|
if (loadedImages[url]) {
|
||||||
|
p[0] = loadedImages[url];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.image.load = function (urls, callback) {
|
||||||
|
var missing = urls.length, loaded = {};
|
||||||
|
if (missing === 0) {
|
||||||
|
callback({});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each(urls, function (i, url) {
|
||||||
|
var handler = function () {
|
||||||
|
--missing;
|
||||||
|
loaded[url] = this;
|
||||||
|
|
||||||
|
if (missing === 0) {
|
||||||
|
callback(loaded);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$('<img />').load(handler).error(handler).attr('src', url);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function drawSeries(plot, ctx, series) {
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
|
||||||
|
if (!series.images || !series.images.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var points = series.datapoints.points,
|
||||||
|
ps = series.datapoints.pointsize;
|
||||||
|
|
||||||
|
for (var i = 0; i < points.length; i += ps) {
|
||||||
|
var img = points[i],
|
||||||
|
x1 = points[i + 1], y1 = points[i + 2],
|
||||||
|
x2 = points[i + 3], y2 = points[i + 4],
|
||||||
|
xaxis = series.xaxis, yaxis = series.yaxis,
|
||||||
|
tmp;
|
||||||
|
|
||||||
|
// actually we should check img.complete, but it
|
||||||
|
// appears to be a somewhat unreliable indicator in
|
||||||
|
// IE6 (false even after load event)
|
||||||
|
if (!img || img.width <= 0 || img.height <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x1 > x2) {
|
||||||
|
tmp = x2;
|
||||||
|
x2 = x1;
|
||||||
|
x1 = tmp;
|
||||||
|
}
|
||||||
|
if (y1 > y2) {
|
||||||
|
tmp = y2;
|
||||||
|
y2 = y1;
|
||||||
|
y1 = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the anchor is at the center of the pixel, expand the
|
||||||
|
// image by 1/2 pixel in each direction
|
||||||
|
if (series.images.anchor === "center") {
|
||||||
|
tmp = 0.5 * (x2 - x1) / (img.width - 1);
|
||||||
|
x1 -= tmp;
|
||||||
|
x2 += tmp;
|
||||||
|
tmp = 0.5 * (y2 - y1) / (img.height - 1);
|
||||||
|
y1 -= tmp;
|
||||||
|
y2 += tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clip
|
||||||
|
if (x1 === x2 || y1 === y2 ||
|
||||||
|
x1 >= xaxis.max || x2 <= xaxis.min ||
|
||||||
|
y1 >= yaxis.max || y2 <= yaxis.min) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sx1 = 0, sy1 = 0, sx2 = img.width, sy2 = img.height;
|
||||||
|
if (x1 < xaxis.min) {
|
||||||
|
sx1 += (sx2 - sx1) * (xaxis.min - x1) / (x2 - x1);
|
||||||
|
x1 = xaxis.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x2 > xaxis.max) {
|
||||||
|
sx2 += (sx2 - sx1) * (xaxis.max - x2) / (x2 - x1);
|
||||||
|
x2 = xaxis.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y1 < yaxis.min) {
|
||||||
|
sy2 += (sy1 - sy2) * (yaxis.min - y1) / (y2 - y1);
|
||||||
|
y1 = yaxis.min;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y2 > yaxis.max) {
|
||||||
|
sy1 += (sy1 - sy2) * (yaxis.max - y2) / (y2 - y1);
|
||||||
|
y2 = yaxis.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
x1 = xaxis.p2c(x1);
|
||||||
|
x2 = xaxis.p2c(x2);
|
||||||
|
y1 = yaxis.p2c(y1);
|
||||||
|
y2 = yaxis.p2c(y2);
|
||||||
|
|
||||||
|
// the transformation may have swapped us
|
||||||
|
if (x1 > x2) {
|
||||||
|
tmp = x2;
|
||||||
|
x2 = x1;
|
||||||
|
x1 = tmp;
|
||||||
|
}
|
||||||
|
if (y1 > y2) {
|
||||||
|
tmp = y2;
|
||||||
|
y2 = y1;
|
||||||
|
y1 = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp = ctx.globalAlpha;
|
||||||
|
ctx.globalAlpha *= series.images.alpha;
|
||||||
|
ctx.drawImage(img,
|
||||||
|
sx1, sy1, sx2 - sx1, sy2 - sy1,
|
||||||
|
x1 + plotOffset.left, y1 + plotOffset.top,
|
||||||
|
x2 - x1, y2 - y1);
|
||||||
|
ctx.globalAlpha = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processRawData(plot, series, data, datapoints) {
|
||||||
|
if (!series.images.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format is Image, x1, y1, x2, y2 (opposite corners)
|
||||||
|
datapoints.format = [
|
||||||
|
{ required: true },
|
||||||
|
{ x: true, number: true, required: true },
|
||||||
|
{ y: true, number: true, required: true },
|
||||||
|
{ x: true, number: true, required: true },
|
||||||
|
{ y: true, number: true, required: true }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processRawData.push(processRawData);
|
||||||
|
plot.hooks.drawSeries.push(drawSeries);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'image',
|
||||||
|
version: '1.1'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
/* Flot plugin for drawing legends.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
var defaultOptions = {
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
noColumns: 1,
|
||||||
|
labelFormatter: null, // fn: string -> string
|
||||||
|
container: null, // container (as jQuery object) to put legend in, null means default on top of graph
|
||||||
|
position: 'ne', // position of default legend container within plot
|
||||||
|
margin: 5, // distance from grid edge to default legend container within plot
|
||||||
|
sorted: null // default to no legend sorting
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function insertLegend(plot, options, placeholder, legendEntries) {
|
||||||
|
// clear before redraw
|
||||||
|
if (options.legend.container != null) {
|
||||||
|
$(options.legend.container).html('');
|
||||||
|
} else {
|
||||||
|
placeholder.find('.legend').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.legend.show) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the legend entries in legend options
|
||||||
|
var entries = options.legend.legendEntries = legendEntries,
|
||||||
|
plotOffset = options.legend.plotOffset = plot.getPlotOffset(),
|
||||||
|
html = [],
|
||||||
|
entry, labelHtml, iconHtml,
|
||||||
|
j = 0,
|
||||||
|
i,
|
||||||
|
pos = "",
|
||||||
|
p = options.legend.position,
|
||||||
|
m = options.legend.margin,
|
||||||
|
shape = {
|
||||||
|
name: '',
|
||||||
|
label: '',
|
||||||
|
xPos: '',
|
||||||
|
yPos: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
html[j++] = '<svg class="legendLayer" style="width:inherit;height:inherit;">';
|
||||||
|
html[j++] = '<rect class="background" width="100%" height="100%"/>';
|
||||||
|
html[j++] = svgShapeDefs;
|
||||||
|
|
||||||
|
var left = 0;
|
||||||
|
var columnWidths = [];
|
||||||
|
var style = window.getComputedStyle(document.querySelector('body'));
|
||||||
|
for (i = 0; i < entries.length; ++i) {
|
||||||
|
var columnIndex = i % options.legend.noColumns;
|
||||||
|
entry = entries[i];
|
||||||
|
shape.label = entry.label;
|
||||||
|
var info = plot.getSurface().getTextInfo('', shape.label, {
|
||||||
|
style: style.fontStyle,
|
||||||
|
variant: style.fontVariant,
|
||||||
|
weight: style.fontWeight,
|
||||||
|
size: parseInt(style.fontSize),
|
||||||
|
lineHeight: parseInt(style.lineHeight),
|
||||||
|
family: style.fontFamily
|
||||||
|
});
|
||||||
|
|
||||||
|
var labelWidth = info.width;
|
||||||
|
// 36px = 1.5em + 6px margin
|
||||||
|
var iconWidth = 48;
|
||||||
|
if (columnWidths[columnIndex]) {
|
||||||
|
if (labelWidth > columnWidths[columnIndex]) {
|
||||||
|
columnWidths[columnIndex] = labelWidth + iconWidth;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
columnWidths[columnIndex] = labelWidth + iconWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate html for icons and labels from a list of entries
|
||||||
|
for (i = 0; i < entries.length; ++i) {
|
||||||
|
var columnIndex = i % options.legend.noColumns;
|
||||||
|
entry = entries[i];
|
||||||
|
iconHtml = '';
|
||||||
|
shape.label = entry.label;
|
||||||
|
shape.xPos = (left + 3) + 'px';
|
||||||
|
left += columnWidths[columnIndex];
|
||||||
|
if ((i + 1) % options.legend.noColumns === 0) {
|
||||||
|
left = 0;
|
||||||
|
}
|
||||||
|
shape.yPos = Math.floor(i / options.legend.noColumns) * 1.5 + 'em';
|
||||||
|
// area
|
||||||
|
if (entry.options.lines.show && entry.options.lines.fill) {
|
||||||
|
shape.name = 'area';
|
||||||
|
shape.fillColor = entry.color;
|
||||||
|
iconHtml += getEntryIconHtml(shape);
|
||||||
|
}
|
||||||
|
// bars
|
||||||
|
if (entry.options.bars.show) {
|
||||||
|
shape.name = 'bar';
|
||||||
|
shape.fillColor = entry.color;
|
||||||
|
iconHtml += getEntryIconHtml(shape);
|
||||||
|
}
|
||||||
|
// lines
|
||||||
|
if (entry.options.lines.show && !entry.options.lines.fill) {
|
||||||
|
shape.name = 'line';
|
||||||
|
shape.strokeColor = entry.color;
|
||||||
|
shape.strokeWidth = entry.options.lines.lineWidth;
|
||||||
|
iconHtml += getEntryIconHtml(shape);
|
||||||
|
}
|
||||||
|
// points
|
||||||
|
if (entry.options.points.show) {
|
||||||
|
shape.name = entry.options.points.symbol;
|
||||||
|
shape.strokeColor = entry.color;
|
||||||
|
shape.fillColor = entry.options.points.fillColor;
|
||||||
|
shape.strokeWidth = entry.options.points.lineWidth;
|
||||||
|
iconHtml += getEntryIconHtml(shape);
|
||||||
|
}
|
||||||
|
|
||||||
|
labelHtml = '<text x="' + shape.xPos + '" y="' + shape.yPos + '" text-anchor="start"><tspan dx="2em" dy="1.2em">' + shape.label + '</tspan></text>'
|
||||||
|
html[j++] = '<g>' + iconHtml + labelHtml + '</g>';
|
||||||
|
}
|
||||||
|
|
||||||
|
html[j++] = '</svg>';
|
||||||
|
if (m[0] == null) {
|
||||||
|
m = [m, m];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.charAt(0) === 'n') {
|
||||||
|
pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
|
||||||
|
} else if (p.charAt(0) === 's') {
|
||||||
|
pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p.charAt(1) === 'e') {
|
||||||
|
pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
|
||||||
|
} else if (p.charAt(1) === 'w') {
|
||||||
|
pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
|
||||||
|
}
|
||||||
|
|
||||||
|
var width = 6;
|
||||||
|
for (i = 0; i < columnWidths.length; ++i) {
|
||||||
|
width += columnWidths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
var legendEl,
|
||||||
|
height = Math.ceil(entries.length / options.legend.noColumns) * 1.6;
|
||||||
|
if (!options.legend.container) {
|
||||||
|
legendEl = $('<div class="legend" style="position:absolute;' + pos + '">' + html.join('') + '</div>').appendTo(placeholder);
|
||||||
|
legendEl.css('width', width + 'px');
|
||||||
|
legendEl.css('height', height + 'em');
|
||||||
|
legendEl.css('pointerEvents', 'none');
|
||||||
|
} else {
|
||||||
|
legendEl = $(html.join('')).appendTo(options.legend.container)[0];
|
||||||
|
options.legend.container.style.width = width + 'px';
|
||||||
|
options.legend.container.style.height = height + 'em';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate html for a shape
|
||||||
|
function getEntryIconHtml(shape) {
|
||||||
|
var html = '',
|
||||||
|
name = shape.name,
|
||||||
|
x = shape.xPos,
|
||||||
|
y = shape.yPos,
|
||||||
|
fill = shape.fillColor,
|
||||||
|
stroke = shape.strokeColor,
|
||||||
|
width = shape.strokeWidth;
|
||||||
|
switch (name) {
|
||||||
|
case 'circle':
|
||||||
|
html = '<use xlink:href="#circle" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'diamond':
|
||||||
|
html = '<use xlink:href="#diamond" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'cross':
|
||||||
|
html = '<use xlink:href="#cross" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
// 'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'rectangle':
|
||||||
|
html = '<use xlink:href="#rectangle" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'plus':
|
||||||
|
html = '<use xlink:href="#plus" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
// 'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'bar':
|
||||||
|
html = '<use xlink:href="#bars" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
'fill="' + fill + '" ' +
|
||||||
|
// 'stroke="' + stroke + '" ' +
|
||||||
|
// 'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'area':
|
||||||
|
html = '<use xlink:href="#area" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
'fill="' + fill + '" ' +
|
||||||
|
// 'stroke="' + stroke + '" ' +
|
||||||
|
// 'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
case 'line':
|
||||||
|
html = '<use xlink:href="#line" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
// 'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// default is circle
|
||||||
|
html = '<use xlink:href="#circle" class="legendIcon" ' +
|
||||||
|
'x="' + x + '" ' +
|
||||||
|
'y="' + y + '" ' +
|
||||||
|
'fill="' + fill + '" ' +
|
||||||
|
'stroke="' + stroke + '" ' +
|
||||||
|
'stroke-width="' + width + '" ' +
|
||||||
|
'width="1.5em" height="1.5em"' +
|
||||||
|
'/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define svg symbols for shapes
|
||||||
|
var svgShapeDefs = '' +
|
||||||
|
'<defs>' +
|
||||||
|
'<symbol id="line" fill="none" viewBox="-5 -5 25 25">' +
|
||||||
|
'<polyline points="0,15 5,5 10,10 15,0"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="area" stroke-width="1" viewBox="-5 -5 25 25">' +
|
||||||
|
'<polyline points="0,15 5,5 10,10 15,0, 15,15, 0,15"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="bars" stroke-width="1" viewBox="-5 -5 25 25">' +
|
||||||
|
'<polyline points="1.5,15.5 1.5,12.5, 4.5,12.5 4.5,15.5 6.5,15.5 6.5,3.5, 9.5,3.5 9.5,15.5 11.5,15.5 11.5,7.5 14.5,7.5 14.5,15.5 1.5,15.5"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="circle" viewBox="-5 -5 25 25">' +
|
||||||
|
'<circle cx="0" cy="15" r="2.5"/>' +
|
||||||
|
'<circle cx="5" cy="5" r="2.5"/>' +
|
||||||
|
'<circle cx="10" cy="10" r="2.5"/>' +
|
||||||
|
'<circle cx="15" cy="0" r="2.5"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="rectangle" viewBox="-5 -5 25 25">' +
|
||||||
|
'<rect x="-2.1" y="12.9" width="4.2" height="4.2"/>' +
|
||||||
|
'<rect x="2.9" y="2.9" width="4.2" height="4.2"/>' +
|
||||||
|
'<rect x="7.9" y="7.9" width="4.2" height="4.2"/>' +
|
||||||
|
'<rect x="12.9" y="-2.1" width="4.2" height="4.2"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="diamond" viewBox="-5 -5 25 25">' +
|
||||||
|
'<path d="M-3,15 L0,12 L3,15, L0,18 Z"/>' +
|
||||||
|
'<path d="M2,5 L5,2 L8,5, L5,8 Z"/>' +
|
||||||
|
'<path d="M7,10 L10,7 L13,10, L10,13 Z"/>' +
|
||||||
|
'<path d="M12,0 L15,-3 L18,0, L15,3 Z"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="cross" fill="none" viewBox="-5 -5 25 25">' +
|
||||||
|
'<path d="M-2.1,12.9 L2.1,17.1, M2.1,12.9 L-2.1,17.1 Z"/>' +
|
||||||
|
'<path d="M2.9,2.9 L7.1,7.1 M7.1,2.9 L2.9,7.1 Z"/>' +
|
||||||
|
'<path d="M7.9,7.9 L12.1,12.1 M12.1,7.9 L7.9,12.1 Z"/>' +
|
||||||
|
'<path d="M12.9,-2.1 L17.1,2.1 M17.1,-2.1 L12.9,2.1 Z"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
|
||||||
|
'<symbol id="plus" fill="none" viewBox="-5 -5 25 25">' +
|
||||||
|
'<path d="M0,12 L0,18, M-3,15 L3,15 Z"/>' +
|
||||||
|
'<path d="M5,2 L5,8 M2,5 L8,5 Z"/>' +
|
||||||
|
'<path d="M10,7 L10,13 M7,10 L13,10 Z"/>' +
|
||||||
|
'<path d="M15,-3 L15,3 M12,0 L18,0 Z"/>' +
|
||||||
|
'</symbol>' +
|
||||||
|
'</defs>';
|
||||||
|
|
||||||
|
// Generate a list of legend entries in their final order
|
||||||
|
function getLegendEntries(series, labelFormatter, sorted) {
|
||||||
|
var lf = labelFormatter,
|
||||||
|
legendEntries = series.reduce(function(validEntries, s, i) {
|
||||||
|
var labelEval = (lf ? lf(s.label, s) : s.label)
|
||||||
|
if (s.hasOwnProperty("label") ? labelEval : true) {
|
||||||
|
var entry = {
|
||||||
|
label: labelEval || 'Plot ' + (i + 1),
|
||||||
|
color: s.color,
|
||||||
|
options: {
|
||||||
|
lines: s.lines,
|
||||||
|
points: s.points,
|
||||||
|
bars: s.bars
|
||||||
|
}
|
||||||
|
}
|
||||||
|
validEntries.push(entry)
|
||||||
|
}
|
||||||
|
return validEntries;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Sort the legend using either the default or a custom comparator
|
||||||
|
if (sorted) {
|
||||||
|
if ($.isFunction(sorted)) {
|
||||||
|
legendEntries.sort(sorted);
|
||||||
|
} else if (sorted === 'reverse') {
|
||||||
|
legendEntries.reverse();
|
||||||
|
} else {
|
||||||
|
var ascending = (sorted !== 'descending');
|
||||||
|
legendEntries.sort(function(a, b) {
|
||||||
|
return a.label === b.label
|
||||||
|
? 0
|
||||||
|
: ((a.label < b.label) !== ascending ? 1 : -1 // Logical XOR
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return legendEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return false if opts1 same as opts2
|
||||||
|
function checkOptions(opts1, opts2) {
|
||||||
|
for (var prop in opts1) {
|
||||||
|
if (opts1.hasOwnProperty(prop)) {
|
||||||
|
if (opts1[prop] !== opts2[prop]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare two lists of legend entries
|
||||||
|
function shouldRedraw(oldEntries, newEntries) {
|
||||||
|
if (!oldEntries || !newEntries) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldEntries.length !== newEntries.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var i, newEntry, oldEntry, newOpts, oldOpts;
|
||||||
|
for (i = 0; i < newEntries.length; i++) {
|
||||||
|
newEntry = newEntries[i];
|
||||||
|
oldEntry = oldEntries[i];
|
||||||
|
|
||||||
|
if (newEntry.label !== oldEntry.label) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newEntry.color !== oldEntry.color) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for changes in lines options
|
||||||
|
newOpts = newEntry.options.lines;
|
||||||
|
oldOpts = oldEntry.options.lines;
|
||||||
|
if (checkOptions(newOpts, oldOpts)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for changes in points options
|
||||||
|
newOpts = newEntry.options.points;
|
||||||
|
oldOpts = oldEntry.options.points;
|
||||||
|
if (checkOptions(newOpts, oldOpts)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for changes in bars options
|
||||||
|
newOpts = newEntry.options.bars;
|
||||||
|
oldOpts = oldEntry.options.bars;
|
||||||
|
if (checkOptions(newOpts, oldOpts)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.setupGrid.push(function (plot) {
|
||||||
|
var options = plot.getOptions();
|
||||||
|
var series = plot.getData(),
|
||||||
|
labelFormatter = options.legend.labelFormatter,
|
||||||
|
oldEntries = options.legend.legendEntries,
|
||||||
|
oldPlotOffset = options.legend.plotOffset,
|
||||||
|
newEntries = getLegendEntries(series, labelFormatter, options.legend.sorted),
|
||||||
|
newPlotOffset = plot.getPlotOffset();
|
||||||
|
|
||||||
|
if (shouldRedraw(oldEntries, newEntries) ||
|
||||||
|
checkOptions(oldPlotOffset, newPlotOffset)) {
|
||||||
|
insertLegend(plot, options, plot.getPlaceholder(), newEntries);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: defaultOptions,
|
||||||
|
name: 'legend',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/* Pretty handling of log axes.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Copyright (c) 2015 Ciprian Ceteras cipix2000@gmail.com.
|
||||||
|
Copyright (c) 2017 Raluca Portase
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
Set axis.mode to "log" to enable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* global jQuery*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
## jquery.flot.logaxis
|
||||||
|
This plugin is used to create logarithmic axis. This includes tick generation,
|
||||||
|
formatters and transformers to and from logarithmic representation.
|
||||||
|
|
||||||
|
### Methods and hooks
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
xaxis: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*tick generators and formatters*/
|
||||||
|
var PREFERRED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 10),
|
||||||
|
EXTENDED_LOG_TICK_VALUES = computePreferedLogTickValues(Number.MAX_VALUE, 4);
|
||||||
|
|
||||||
|
function computePreferedLogTickValues(endLimit, rangeStep) {
|
||||||
|
var log10End = Math.floor(Math.log(endLimit) * Math.LOG10E) - 1,
|
||||||
|
log10Start = -log10End,
|
||||||
|
val, range, vals = [];
|
||||||
|
|
||||||
|
for (var power = log10Start; power <= log10End; power++) {
|
||||||
|
range = parseFloat('1e' + power);
|
||||||
|
for (var mult = 1; mult < 9; mult += rangeStep) {
|
||||||
|
val = range * mult;
|
||||||
|
vals.push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vals;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- logTickGenerator(plot, axis, noTicks)
|
||||||
|
|
||||||
|
Generates logarithmic ticks, depending on axis range.
|
||||||
|
In case the number of ticks that can be generated is less than the expected noTicks/4,
|
||||||
|
a linear tick generation is used.
|
||||||
|
*/
|
||||||
|
var logTickGenerator = function (plot, axis, noTicks) {
|
||||||
|
var ticks = [],
|
||||||
|
minIdx = -1,
|
||||||
|
maxIdx = -1,
|
||||||
|
surface = plot.getCanvas(),
|
||||||
|
logTickValues = PREFERRED_LOG_TICK_VALUES,
|
||||||
|
min = clampAxis(axis, plot),
|
||||||
|
max = axis.max;
|
||||||
|
|
||||||
|
if (!noTicks) {
|
||||||
|
noTicks = 0.3 * Math.sqrt(axis.direction === "x" ? surface.width : surface.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
PREFERRED_LOG_TICK_VALUES.some(function (val, i) {
|
||||||
|
if (val >= min) {
|
||||||
|
minIdx = i;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
PREFERRED_LOG_TICK_VALUES.some(function (val, i) {
|
||||||
|
if (val >= max) {
|
||||||
|
maxIdx = i;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (maxIdx === -1) {
|
||||||
|
maxIdx = PREFERRED_LOG_TICK_VALUES.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxIdx - minIdx <= noTicks / 4 && logTickValues.length !== EXTENDED_LOG_TICK_VALUES.length) {
|
||||||
|
//try with multiple of 5 for tick values
|
||||||
|
logTickValues = EXTENDED_LOG_TICK_VALUES;
|
||||||
|
minIdx *= 2;
|
||||||
|
maxIdx *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastDisplayed = null,
|
||||||
|
inverseNoTicks = 1 / noTicks,
|
||||||
|
tickValue, pixelCoord, tick;
|
||||||
|
|
||||||
|
// Count the number of tick values would appear, if we can get at least
|
||||||
|
// nTicks / 4 accept them.
|
||||||
|
if (maxIdx - minIdx >= noTicks / 4) {
|
||||||
|
for (var idx = maxIdx; idx >= minIdx; idx--) {
|
||||||
|
tickValue = logTickValues[idx];
|
||||||
|
pixelCoord = (Math.log(tickValue) - Math.log(min)) / (Math.log(max) - Math.log(min));
|
||||||
|
tick = tickValue;
|
||||||
|
|
||||||
|
if (lastDisplayed === null) {
|
||||||
|
lastDisplayed = {
|
||||||
|
pixelCoord: pixelCoord,
|
||||||
|
idealPixelCoord: pixelCoord
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (Math.abs(pixelCoord - lastDisplayed.pixelCoord) >= inverseNoTicks) {
|
||||||
|
lastDisplayed = {
|
||||||
|
pixelCoord: pixelCoord,
|
||||||
|
idealPixelCoord: lastDisplayed.idealPixelCoord - inverseNoTicks
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
tick = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tick) {
|
||||||
|
ticks.push(tick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Since we went in backwards order.
|
||||||
|
ticks.reverse();
|
||||||
|
} else {
|
||||||
|
var tickSize = plot.computeTickSize(min, max, noTicks),
|
||||||
|
customAxis = {min: min, max: max, tickSize: tickSize};
|
||||||
|
ticks = $.plot.linearTickGenerator(customAxis);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ticks;
|
||||||
|
};
|
||||||
|
|
||||||
|
var clampAxis = function (axis, plot) {
|
||||||
|
var min = axis.min,
|
||||||
|
max = axis.max;
|
||||||
|
|
||||||
|
if (min <= 0) {
|
||||||
|
//for empty graph if axis.min is not strictly positive make it 0.1
|
||||||
|
if (axis.datamin === null) {
|
||||||
|
min = axis.min = 0.1;
|
||||||
|
} else {
|
||||||
|
min = processAxisOffset(plot, axis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max < min) {
|
||||||
|
axis.max = axis.datamax !== null ? axis.datamax : axis.options.max;
|
||||||
|
axis.options.offset.below = 0;
|
||||||
|
axis.options.offset.above = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- logTickFormatter(value, axis, precision)
|
||||||
|
|
||||||
|
This is the corresponding tickFormatter of the logaxis.
|
||||||
|
For a number greater that 10^6 or smaller than 10^(-3), this will be drawn
|
||||||
|
with e representation
|
||||||
|
*/
|
||||||
|
var logTickFormatter = function (value, axis, precision) {
|
||||||
|
var tenExponent = value > 0 ? Math.floor(Math.log(value) / Math.LN10) : 0;
|
||||||
|
|
||||||
|
if (precision) {
|
||||||
|
if ((tenExponent >= -4) && (tenExponent <= 7)) {
|
||||||
|
return $.plot.defaultTickFormatter(value, axis, precision);
|
||||||
|
} else {
|
||||||
|
return $.plot.expRepTickFormatter(value, axis, precision);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((tenExponent >= -4) && (tenExponent <= 7)) {
|
||||||
|
//if we have float numbers, return a limited length string(ex: 0.0009 is represented as 0.000900001)
|
||||||
|
var formattedValue = tenExponent < 0 ? value.toFixed(-tenExponent) : value.toFixed(tenExponent + 2);
|
||||||
|
if (formattedValue.indexOf('.') !== -1) {
|
||||||
|
var lastZero = formattedValue.lastIndexOf('0');
|
||||||
|
|
||||||
|
while (lastZero === formattedValue.length - 1) {
|
||||||
|
formattedValue = formattedValue.slice(0, -1);
|
||||||
|
lastZero = formattedValue.lastIndexOf('0');
|
||||||
|
}
|
||||||
|
|
||||||
|
//delete the dot if is last
|
||||||
|
if (formattedValue.indexOf('.') === formattedValue.length - 1) {
|
||||||
|
formattedValue = formattedValue.slice(0, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return formattedValue;
|
||||||
|
} else {
|
||||||
|
return $.plot.expRepTickFormatter(value, axis);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*logaxis caracteristic functions*/
|
||||||
|
var logTransform = function (v) {
|
||||||
|
if (v < PREFERRED_LOG_TICK_VALUES[0]) {
|
||||||
|
v = PREFERRED_LOG_TICK_VALUES[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.log(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
var logInverseTransform = function (v) {
|
||||||
|
return Math.exp(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
var invertedTransform = function (v) {
|
||||||
|
return -v;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invertedLogTransform = function (v) {
|
||||||
|
return -logTransform(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
var invertedLogInverseTransform = function (v) {
|
||||||
|
return logInverseTransform(-v);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
- setDataminRange(plot, axis)
|
||||||
|
|
||||||
|
It is used for clamping the starting point of a logarithmic axis.
|
||||||
|
This will set the axis datamin range to 0.1 or to the first datapoint greater then 0.
|
||||||
|
The function is usefull since the logarithmic representation can not show
|
||||||
|
values less than or equal to 0.
|
||||||
|
*/
|
||||||
|
function setDataminRange(plot, axis) {
|
||||||
|
if (axis.options.mode === 'log' && axis.datamin <= 0) {
|
||||||
|
if (axis.datamin === null) {
|
||||||
|
axis.datamin = 0.1;
|
||||||
|
} else {
|
||||||
|
axis.datamin = processAxisOffset(plot, axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAxisOffset(plot, axis) {
|
||||||
|
var series = plot.getData(),
|
||||||
|
range = series
|
||||||
|
.filter(function(series) {
|
||||||
|
return series.xaxis === axis || series.yaxis === axis;
|
||||||
|
})
|
||||||
|
.map(function(series) {
|
||||||
|
return plot.computeRangeForDataSeries(series, null, isValid);
|
||||||
|
}),
|
||||||
|
min = axis.direction === 'x'
|
||||||
|
? Math.min(0.1, range && range[0] ? range[0].xmin : 0.1)
|
||||||
|
: Math.min(0.1, range && range[0] ? range[0].ymin : 0.1);
|
||||||
|
|
||||||
|
axis.min = min;
|
||||||
|
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValid(a) {
|
||||||
|
return a > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processOptions.push(function (plot) {
|
||||||
|
$.each(plot.getAxes(), function (axisName, axis) {
|
||||||
|
var opts = axis.options;
|
||||||
|
if (opts.mode === 'log') {
|
||||||
|
axis.tickGenerator = function (axis) {
|
||||||
|
var noTicks = 11;
|
||||||
|
return logTickGenerator(plot, axis, noTicks);
|
||||||
|
};
|
||||||
|
if (typeof axis.options.tickFormatter !== 'function') {
|
||||||
|
axis.options.tickFormatter = logTickFormatter;
|
||||||
|
}
|
||||||
|
axis.options.transform = opts.inverted ? invertedLogTransform : logTransform;
|
||||||
|
axis.options.inverseTransform = opts.inverted ? invertedLogInverseTransform : logInverseTransform;
|
||||||
|
axis.options.autoScaleMargin = 0;
|
||||||
|
plot.hooks.setRange.push(setDataminRange);
|
||||||
|
} else if (opts.inverted) {
|
||||||
|
axis.options.transform = invertedTransform;
|
||||||
|
axis.options.inverseTransform = invertedTransform;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'log',
|
||||||
|
version: '0.1'
|
||||||
|
});
|
||||||
|
|
||||||
|
$.plot.logTicksGenerator = logTickGenerator;
|
||||||
|
$.plot.logTickFormatter = logTickFormatter;
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,798 @@
|
|||||||
|
/* Flot plugin for adding the ability to pan and zoom the plot.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Copyright (c) 2016 Ciprian Ceteras.
|
||||||
|
Copyright (c) 2017 Raluca Portase.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
## jquery.flot.navigate.js
|
||||||
|
|
||||||
|
This flot plugin is used for adding the ability to pan and zoom the plot.
|
||||||
|
A higher level overview is available at [interactions](interactions.md) documentation.
|
||||||
|
|
||||||
|
The default behaviour is scrollwheel up/down to zoom in, drag
|
||||||
|
to pan. The plugin defines plot.zoom({ center }), plot.zoomOut() and
|
||||||
|
plot.pan( offset ) so you easily can add custom controls. It also fires
|
||||||
|
"plotpan" and "plotzoom" events, useful for synchronizing plots.
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
```js
|
||||||
|
zoom: {
|
||||||
|
interactive: false,
|
||||||
|
active: false,
|
||||||
|
amount: 1.5 // 2 = 200% (zoom in), 0.5 = 50% (zoom out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pan: {
|
||||||
|
interactive: false,
|
||||||
|
active: false,
|
||||||
|
cursor: "move", // CSS mouse cursor value used when dragging, e.g. "pointer"
|
||||||
|
frameRate: 60,
|
||||||
|
mode: "smart" // enable smart pan mode
|
||||||
|
}
|
||||||
|
|
||||||
|
xaxis: {
|
||||||
|
axisZoom: true, //zoom axis when mouse over it is allowed
|
||||||
|
plotZoom: true, //zoom axis is allowed for plot zoom
|
||||||
|
axisPan: true, //pan axis when mouse over it is allowed
|
||||||
|
plotPan: true //pan axis is allowed for plot pan
|
||||||
|
}
|
||||||
|
|
||||||
|
yaxis: {
|
||||||
|
axisZoom: true, //zoom axis when mouse over it is allowed
|
||||||
|
plotZoom: true, //zoom axis is allowed for plot zoom
|
||||||
|
axisPan: true, //pan axis when mouse over it is allowed
|
||||||
|
plotPan: true //pan axis is allowed for plot pan
|
||||||
|
}
|
||||||
|
```
|
||||||
|
**interactive** enables the built-in drag/click behaviour. If you enable
|
||||||
|
interactive for pan, then you'll have a basic plot that supports moving
|
||||||
|
around; the same for zoom.
|
||||||
|
|
||||||
|
**active** is true after a touch tap on plot. This enables plot navigation.
|
||||||
|
Once activated, zoom and pan cannot be deactivated. When the plot becomes active,
|
||||||
|
"plotactivated" event is triggered.
|
||||||
|
|
||||||
|
**amount** specifies the default amount to zoom in (so 1.5 = 150%) relative to
|
||||||
|
the current viewport.
|
||||||
|
|
||||||
|
**cursor** is a standard CSS mouse cursor string used for visual feedback to the
|
||||||
|
user when dragging.
|
||||||
|
|
||||||
|
**frameRate** specifies the maximum number of times per second the plot will
|
||||||
|
update itself while the user is panning around on it (set to null to disable
|
||||||
|
intermediate pans, the plot will then not update until the mouse button is
|
||||||
|
released).
|
||||||
|
|
||||||
|
**mode** a string specifies the pan mode for mouse interaction. Accepted values:
|
||||||
|
'manual': no pan hint or direction snapping;
|
||||||
|
'smart': The graph shows pan hint bar and the pan movement will snap
|
||||||
|
to one direction when the drag direction is close to it;
|
||||||
|
'smartLock'. The graph shows pan hint bar and the pan movement will always
|
||||||
|
snap to a direction that the drag diorection started with.
|
||||||
|
|
||||||
|
Example API usage:
|
||||||
|
```js
|
||||||
|
plot = $.plot(...);
|
||||||
|
|
||||||
|
// zoom default amount in on the pixel ( 10, 20 )
|
||||||
|
plot.zoom({ center: { left: 10, top: 20 } });
|
||||||
|
|
||||||
|
// zoom out again
|
||||||
|
plot.zoomOut({ center: { left: 10, top: 20 } });
|
||||||
|
|
||||||
|
// zoom 200% in on the pixel (10, 20)
|
||||||
|
plot.zoom({ amount: 2, center: { left: 10, top: 20 } });
|
||||||
|
|
||||||
|
// pan 100 pixels to the left (changing x-range in a positive way) and 20 down
|
||||||
|
plot.pan({ left: -100, top: 20 })
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, "center" specifies where the center of the zooming should happen. Note
|
||||||
|
that this is defined in pixel space, not the space of the data points (you can
|
||||||
|
use the p2c helpers on the axes in Flot to help you convert between these).
|
||||||
|
|
||||||
|
**amount** is the amount to zoom the viewport relative to the current range, so
|
||||||
|
1 is 100% (i.e. no change), 1.5 is 150% (zoom in), 0.7 is 70% (zoom out). You
|
||||||
|
can set the default in the options.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-enable */
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
zoom: {
|
||||||
|
interactive: false,
|
||||||
|
active: false,
|
||||||
|
amount: 1.5 // how much to zoom relative to current position, 2 = 200% (zoom in), 0.5 = 50% (zoom out)
|
||||||
|
},
|
||||||
|
pan: {
|
||||||
|
interactive: false,
|
||||||
|
active: false,
|
||||||
|
cursor: "move",
|
||||||
|
frameRate: 60,
|
||||||
|
mode: 'smart'
|
||||||
|
},
|
||||||
|
recenter: {
|
||||||
|
interactive: true
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
axisZoom: true, //zoom axis when mouse over it is allowed
|
||||||
|
plotZoom: true, //zoom axis is allowed for plot zoom
|
||||||
|
axisPan: true, //pan axis when mouse over it is allowed
|
||||||
|
plotPan: true //pan axis is allowed for plot pan
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
axisZoom: true,
|
||||||
|
plotZoom: true,
|
||||||
|
axisPan: true,
|
||||||
|
plotPan: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var saturated = $.plot.saturated;
|
||||||
|
var browser = $.plot.browser;
|
||||||
|
var SNAPPING_CONSTANT = $.plot.uiConstants.SNAPPING_CONSTANT;
|
||||||
|
var PANHINT_LENGTH_CONSTANT = $.plot.uiConstants.PANHINT_LENGTH_CONSTANT;
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processOptions.push(initNevigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initNevigation(plot, options) {
|
||||||
|
var panAxes = null;
|
||||||
|
var canDrag = false;
|
||||||
|
var useManualPan = options.pan.mode === 'manual',
|
||||||
|
smartPanLock = options.pan.mode === 'smartLock',
|
||||||
|
useSmartPan = smartPanLock || options.pan.mode === 'smart';
|
||||||
|
|
||||||
|
function onZoomClick(e, zoomOut, amount) {
|
||||||
|
var page = browser.getPageXY(e);
|
||||||
|
|
||||||
|
var c = plot.offset();
|
||||||
|
c.left = page.X - c.left;
|
||||||
|
c.top = page.Y - c.top;
|
||||||
|
|
||||||
|
var ec = plot.getPlaceholder().offset();
|
||||||
|
ec.left = page.X - ec.left;
|
||||||
|
ec.top = page.Y - ec.top;
|
||||||
|
|
||||||
|
var axes = plot.getXAxes().concat(plot.getYAxes()).filter(function (axis) {
|
||||||
|
var box = axis.box;
|
||||||
|
if (box !== undefined) {
|
||||||
|
return (ec.left > box.left) && (ec.left < box.left + box.width) &&
|
||||||
|
(ec.top > box.top) && (ec.top < box.top + box.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (axes.length === 0) {
|
||||||
|
axes = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zoomOut) {
|
||||||
|
plot.zoomOut({
|
||||||
|
center: c,
|
||||||
|
axes: axes,
|
||||||
|
amount: amount
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
plot.zoom({
|
||||||
|
center: c,
|
||||||
|
axes: axes,
|
||||||
|
amount: amount
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevCursor = 'default',
|
||||||
|
panHint = null,
|
||||||
|
panTimeout = null,
|
||||||
|
plotState,
|
||||||
|
prevDragPosition = { x: 0, y: 0 },
|
||||||
|
isPanAction = false;
|
||||||
|
|
||||||
|
function onMouseWheel(e, delta) {
|
||||||
|
var maxAbsoluteDeltaOnMac = 1,
|
||||||
|
isMacScroll = Math.abs(e.originalEvent.deltaY) <= maxAbsoluteDeltaOnMac,
|
||||||
|
defaultNonMacScrollAmount = null,
|
||||||
|
macMagicRatio = 50,
|
||||||
|
amount = isMacScroll ? 1 + Math.abs(e.originalEvent.deltaY) / macMagicRatio : defaultNonMacScrollAmount;
|
||||||
|
|
||||||
|
if (isPanAction) {
|
||||||
|
onDragEnd(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plot.getOptions().zoom.active) {
|
||||||
|
e.preventDefault();
|
||||||
|
onZoomClick(e, delta < 0, amount);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.navigationState = function(startPageX, startPageY) {
|
||||||
|
var axes = this.getAxes();
|
||||||
|
var result = {};
|
||||||
|
Object.keys(axes).forEach(function(axisName) {
|
||||||
|
var axis = axes[axisName];
|
||||||
|
result[axisName] = {
|
||||||
|
navigationOffset: { below: axis.options.offset.below || 0,
|
||||||
|
above: axis.options.offset.above || 0},
|
||||||
|
axisMin: axis.min,
|
||||||
|
axisMax: axis.max,
|
||||||
|
diagMode: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
result.startPageX = startPageX || 0;
|
||||||
|
result.startPageY = startPageY || 0;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseDown(e) {
|
||||||
|
canDrag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp(e) {
|
||||||
|
canDrag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLeftMouseButtonPressed(e) {
|
||||||
|
return e.button === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(e) {
|
||||||
|
if (!canDrag || !isLeftMouseButtonPressed(e)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPanAction = true;
|
||||||
|
var page = browser.getPageXY(e);
|
||||||
|
|
||||||
|
var ec = plot.getPlaceholder().offset();
|
||||||
|
ec.left = page.X - ec.left;
|
||||||
|
ec.top = page.Y - ec.top;
|
||||||
|
|
||||||
|
panAxes = plot.getXAxes().concat(plot.getYAxes()).filter(function (axis) {
|
||||||
|
var box = axis.box;
|
||||||
|
if (box !== undefined) {
|
||||||
|
return (ec.left > box.left) && (ec.left < box.left + box.width) &&
|
||||||
|
(ec.top > box.top) && (ec.top < box.top + box.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (panAxes.length === 0) {
|
||||||
|
panAxes = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = plot.getPlaceholder().css('cursor');
|
||||||
|
if (c) {
|
||||||
|
prevCursor = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.getPlaceholder().css('cursor', plot.getOptions().pan.cursor);
|
||||||
|
|
||||||
|
if (useSmartPan) {
|
||||||
|
plotState = plot.navigationState(page.X, page.Y);
|
||||||
|
} else if (useManualPan) {
|
||||||
|
prevDragPosition.x = page.X;
|
||||||
|
prevDragPosition.y = page.Y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag(e) {
|
||||||
|
if (!isPanAction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var page = browser.getPageXY(e);
|
||||||
|
var frameRate = plot.getOptions().pan.frameRate;
|
||||||
|
|
||||||
|
if (frameRate === -1) {
|
||||||
|
if (useSmartPan) {
|
||||||
|
plot.smartPan({
|
||||||
|
x: plotState.startPageX - page.X,
|
||||||
|
y: plotState.startPageY - page.Y
|
||||||
|
}, plotState, panAxes, false, smartPanLock);
|
||||||
|
} else if (useManualPan) {
|
||||||
|
plot.pan({
|
||||||
|
left: prevDragPosition.x - page.X,
|
||||||
|
top: prevDragPosition.y - page.Y,
|
||||||
|
axes: panAxes
|
||||||
|
});
|
||||||
|
prevDragPosition.x = page.X;
|
||||||
|
prevDragPosition.y = page.Y;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panTimeout || !frameRate) return;
|
||||||
|
|
||||||
|
panTimeout = setTimeout(function() {
|
||||||
|
if (useSmartPan) {
|
||||||
|
plot.smartPan({
|
||||||
|
x: plotState.startPageX - page.X,
|
||||||
|
y: plotState.startPageY - page.Y
|
||||||
|
}, plotState, panAxes, false, smartPanLock);
|
||||||
|
} else if (useManualPan) {
|
||||||
|
plot.pan({
|
||||||
|
left: prevDragPosition.x - page.X,
|
||||||
|
top: prevDragPosition.y - page.Y,
|
||||||
|
axes: panAxes
|
||||||
|
});
|
||||||
|
prevDragPosition.x = page.X;
|
||||||
|
prevDragPosition.y = page.Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
panTimeout = null;
|
||||||
|
}, 1 / frameRate * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd(e) {
|
||||||
|
if (!isPanAction) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panTimeout) {
|
||||||
|
clearTimeout(panTimeout);
|
||||||
|
panTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPanAction = false;
|
||||||
|
var page = browser.getPageXY(e);
|
||||||
|
|
||||||
|
plot.getPlaceholder().css('cursor', prevCursor);
|
||||||
|
|
||||||
|
if (useSmartPan) {
|
||||||
|
plot.smartPan({
|
||||||
|
x: plotState.startPageX - page.X,
|
||||||
|
y: plotState.startPageY - page.Y
|
||||||
|
}, plotState, panAxes, false, smartPanLock);
|
||||||
|
plot.smartPan.end();
|
||||||
|
} else if (useManualPan) {
|
||||||
|
plot.pan({
|
||||||
|
left: prevDragPosition.x - page.X,
|
||||||
|
top: prevDragPosition.y - page.Y,
|
||||||
|
axes: panAxes
|
||||||
|
});
|
||||||
|
prevDragPosition.x = 0;
|
||||||
|
prevDragPosition.y = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDblClick(e) {
|
||||||
|
plot.activate();
|
||||||
|
var o = plot.getOptions()
|
||||||
|
|
||||||
|
if (!o.recenter.interactive) { return; }
|
||||||
|
|
||||||
|
var axes = plot.getTouchedAxis(e.clientX, e.clientY),
|
||||||
|
event;
|
||||||
|
|
||||||
|
plot.recenter({ axes: axes[0] ? axes : null });
|
||||||
|
|
||||||
|
if (axes[0]) {
|
||||||
|
event = new $.Event('re-center', { detail: {
|
||||||
|
axisTouched: axes[0]
|
||||||
|
}});
|
||||||
|
} else {
|
||||||
|
event = new $.Event('re-center', { detail: e });
|
||||||
|
}
|
||||||
|
plot.getPlaceholder().trigger(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(e) {
|
||||||
|
plot.activate();
|
||||||
|
|
||||||
|
if (isPanAction) {
|
||||||
|
onDragEnd(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.activate = function() {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
if (!o.pan.active || !o.zoom.active) {
|
||||||
|
o.pan.active = true;
|
||||||
|
o.zoom.active = true;
|
||||||
|
plot.getPlaceholder().trigger("plotactivated", [plot]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents(plot, eventHolder) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
if (o.zoom.interactive) {
|
||||||
|
eventHolder.mousewheel(onMouseWheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.pan.interactive) {
|
||||||
|
plot.addEventHandler("dragstart", onDragStart, eventHolder, 0);
|
||||||
|
plot.addEventHandler("drag", onDrag, eventHolder, 0);
|
||||||
|
plot.addEventHandler("dragend", onDragEnd, eventHolder, 0);
|
||||||
|
eventHolder.bind("mousedown", onMouseDown);
|
||||||
|
eventHolder.bind("mouseup", onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHolder.dblclick(onDblClick);
|
||||||
|
eventHolder.click(onClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.zoomOut = function(args) {
|
||||||
|
if (!args) {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.amount) {
|
||||||
|
args.amount = plot.getOptions().zoom.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
args.amount = 1 / args.amount;
|
||||||
|
plot.zoom(args);
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.zoom = function(args) {
|
||||||
|
if (!args) {
|
||||||
|
args = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = args.center,
|
||||||
|
amount = args.amount || plot.getOptions().zoom.amount,
|
||||||
|
w = plot.width(),
|
||||||
|
h = plot.height(),
|
||||||
|
axes = args.axes || plot.getAxes();
|
||||||
|
|
||||||
|
if (!c) {
|
||||||
|
c = {
|
||||||
|
left: w / 2,
|
||||||
|
top: h / 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var xf = c.left / w,
|
||||||
|
yf = c.top / h,
|
||||||
|
minmax = {
|
||||||
|
x: {
|
||||||
|
min: c.left - xf * w / amount,
|
||||||
|
max: c.left + (1 - xf) * w / amount
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
min: c.top - yf * h / amount,
|
||||||
|
max: c.top + (1 - yf) * h / amount
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (var key in axes) {
|
||||||
|
if (!axes.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var axis = axes[key],
|
||||||
|
opts = axis.options,
|
||||||
|
min = minmax[axis.direction].min,
|
||||||
|
max = minmax[axis.direction].max,
|
||||||
|
navigationOffset = axis.options.offset;
|
||||||
|
|
||||||
|
//skip axis without axisZoom when zooming only on certain axis or axis without plotZoom for zoom on entire plot
|
||||||
|
if ((!opts.axisZoom && args.axes) || (!args.axes && !opts.plotZoom)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
min = $.plot.saturated.saturate(axis.c2p(min));
|
||||||
|
max = $.plot.saturated.saturate(axis.c2p(max));
|
||||||
|
if (min > max) {
|
||||||
|
// make sure min < max
|
||||||
|
var tmp = min;
|
||||||
|
min = max;
|
||||||
|
max = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offsetBelow = $.plot.saturated.saturate(navigationOffset.below - (axis.min - min));
|
||||||
|
var offsetAbove = $.plot.saturated.saturate(navigationOffset.above - (axis.max - max));
|
||||||
|
opts.offset = { below: offsetBelow, above: offsetAbove };
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.setupGrid(true);
|
||||||
|
plot.draw();
|
||||||
|
|
||||||
|
if (!args.preventEvent) {
|
||||||
|
plot.getPlaceholder().trigger("plotzoom", [plot, args]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.pan = function(args) {
|
||||||
|
var delta = {
|
||||||
|
x: +args.left,
|
||||||
|
y: +args.top
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isNaN(delta.x)) delta.x = 0;
|
||||||
|
if (isNaN(delta.y)) delta.y = 0;
|
||||||
|
|
||||||
|
$.each(args.axes || plot.getAxes(), function(_, axis) {
|
||||||
|
var opts = axis.options,
|
||||||
|
d = delta[axis.direction];
|
||||||
|
|
||||||
|
//skip axis without axisPan when panning only on certain axis or axis without plotPan for pan the entire plot
|
||||||
|
if ((!opts.axisPan && args.axes) || (!opts.plotPan && !args.axes)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d !== 0) {
|
||||||
|
var navigationOffsetBelow = saturated.saturate(axis.c2p(axis.p2c(axis.min) + d) - axis.c2p(axis.p2c(axis.min))),
|
||||||
|
navigationOffsetAbove = saturated.saturate(axis.c2p(axis.p2c(axis.max) + d) - axis.c2p(axis.p2c(axis.max)));
|
||||||
|
|
||||||
|
if (!isFinite(navigationOffsetBelow)) {
|
||||||
|
navigationOffsetBelow = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFinite(navigationOffsetAbove)) {
|
||||||
|
navigationOffsetAbove = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.offset = {
|
||||||
|
below: saturated.saturate(navigationOffsetBelow + (opts.offset.below || 0)),
|
||||||
|
above: saturated.saturate(navigationOffsetAbove + (opts.offset.above || 0))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.setupGrid(true);
|
||||||
|
plot.draw();
|
||||||
|
if (!args.preventEvent) {
|
||||||
|
plot.getPlaceholder().trigger("plotpan", [plot, args]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.recenter = function(args) {
|
||||||
|
$.each(args.axes || plot.getAxes(), function(_, axis) {
|
||||||
|
if (args.axes) {
|
||||||
|
if (this.direction === 'x') {
|
||||||
|
axis.options.offset = { below: 0 };
|
||||||
|
} else if (this.direction === 'y') {
|
||||||
|
axis.options.offset = { above: 0 };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
axis.options.offset = { below: 0, above: 0 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
plot.setupGrid(true);
|
||||||
|
plot.draw();
|
||||||
|
};
|
||||||
|
|
||||||
|
var shouldSnap = function(delta) {
|
||||||
|
return (Math.abs(delta.y) < SNAPPING_CONSTANT && Math.abs(delta.x) >= SNAPPING_CONSTANT) ||
|
||||||
|
(Math.abs(delta.x) < SNAPPING_CONSTANT && Math.abs(delta.y) >= SNAPPING_CONSTANT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// adjust delta so the pan action is constrained on the vertical or horizontal direction
|
||||||
|
// it the movements in the other direction are small
|
||||||
|
var adjustDeltaToSnap = function(delta) {
|
||||||
|
if (Math.abs(delta.x) < SNAPPING_CONSTANT && Math.abs(delta.y) >= SNAPPING_CONSTANT) {
|
||||||
|
return {x: 0, y: delta.y};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(delta.y) < SNAPPING_CONSTANT && Math.abs(delta.x) >= SNAPPING_CONSTANT) {
|
||||||
|
return {x: delta.x, y: 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
return delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lockedDirection = null;
|
||||||
|
var lockDeltaDirection = function(delta) {
|
||||||
|
if (!lockedDirection && Math.max(Math.abs(delta.x), Math.abs(delta.y)) >= SNAPPING_CONSTANT) {
|
||||||
|
lockedDirection = Math.abs(delta.x) < Math.abs(delta.y) ? 'y' : 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (lockedDirection) {
|
||||||
|
case 'x':
|
||||||
|
return { x: delta.x, y: 0 };
|
||||||
|
case 'y':
|
||||||
|
return { x: 0, y: delta.y };
|
||||||
|
default:
|
||||||
|
return { x: 0, y: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDiagonalMode = function(delta) {
|
||||||
|
if (Math.abs(delta.x) > 0 && Math.abs(delta.y) > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var restoreAxisOffset = function(axes, initialState, delta) {
|
||||||
|
var axis;
|
||||||
|
Object.keys(axes).forEach(function(axisName) {
|
||||||
|
axis = axes[axisName];
|
||||||
|
if (delta[axis.direction] === 0) {
|
||||||
|
axis.options.offset.below = initialState[axisName].navigationOffset.below;
|
||||||
|
axis.options.offset.above = initialState[axisName].navigationOffset.above;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevDelta = { x: 0, y: 0 };
|
||||||
|
plot.smartPan = function(delta, initialState, panAxes, preventEvent, smartLock) {
|
||||||
|
var snap = smartLock ? true : shouldSnap(delta),
|
||||||
|
axes = plot.getAxes(),
|
||||||
|
opts;
|
||||||
|
delta = smartLock ? lockDeltaDirection(delta) : adjustDeltaToSnap(delta);
|
||||||
|
|
||||||
|
if (isDiagonalMode(delta)) {
|
||||||
|
initialState.diagMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snap && initialState.diagMode === true) {
|
||||||
|
initialState.diagMode = false;
|
||||||
|
restoreAxisOffset(axes, initialState, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snap) {
|
||||||
|
panHint = {
|
||||||
|
start: {
|
||||||
|
x: initialState.startPageX - plot.offset().left + plot.getPlotOffset().left,
|
||||||
|
y: initialState.startPageY - plot.offset().top + plot.getPlotOffset().top
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: initialState.startPageX - delta.x - plot.offset().left + plot.getPlotOffset().left,
|
||||||
|
y: initialState.startPageY - delta.y - plot.offset().top + plot.getPlotOffset().top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panHint = {
|
||||||
|
start: {
|
||||||
|
x: initialState.startPageX - plot.offset().left + plot.getPlotOffset().left,
|
||||||
|
y: initialState.startPageY - plot.offset().top + plot.getPlotOffset().top
|
||||||
|
},
|
||||||
|
end: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(delta.x)) delta.x = 0;
|
||||||
|
if (isNaN(delta.y)) delta.y = 0;
|
||||||
|
|
||||||
|
if (panAxes) {
|
||||||
|
axes = panAxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
var axis, axisMin, axisMax, p, d;
|
||||||
|
Object.keys(axes).forEach(function(axisName) {
|
||||||
|
axis = axes[axisName];
|
||||||
|
axisMin = axis.min;
|
||||||
|
axisMax = axis.max;
|
||||||
|
opts = axis.options;
|
||||||
|
|
||||||
|
d = delta[axis.direction];
|
||||||
|
p = prevDelta[axis.direction];
|
||||||
|
|
||||||
|
//skip axis without axisPan when panning only on certain axis or axis without plotPan for pan the entire plot
|
||||||
|
if ((!opts.axisPan && panAxes) || (!panAxes && !opts.plotPan)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d !== 0) {
|
||||||
|
var navigationOffsetBelow = saturated.saturate(axis.c2p(axis.p2c(axisMin) - (p - d)) - axis.c2p(axis.p2c(axisMin))),
|
||||||
|
navigationOffsetAbove = saturated.saturate(axis.c2p(axis.p2c(axisMax) - (p - d)) - axis.c2p(axis.p2c(axisMax)));
|
||||||
|
|
||||||
|
if (!isFinite(navigationOffsetBelow)) {
|
||||||
|
navigationOffsetBelow = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFinite(navigationOffsetAbove)) {
|
||||||
|
navigationOffsetAbove = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
axis.options.offset.below = saturated.saturate(navigationOffsetBelow + (axis.options.offset.below || 0));
|
||||||
|
axis.options.offset.above = saturated.saturate(navigationOffsetAbove + (axis.options.offset.above || 0));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
prevDelta = delta;
|
||||||
|
plot.setupGrid(true);
|
||||||
|
plot.draw();
|
||||||
|
|
||||||
|
if (!preventEvent) {
|
||||||
|
plot.getPlaceholder().trigger("plotpan", [plot, delta, panAxes, initialState]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
plot.smartPan.end = function() {
|
||||||
|
panHint = null;
|
||||||
|
lockedDirection = null;
|
||||||
|
prevDelta = { x: 0, y: 0 };
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(plot, eventHolder) {
|
||||||
|
eventHolder.unbind("mousewheel", onMouseWheel);
|
||||||
|
eventHolder.unbind("mousedown", onMouseDown);
|
||||||
|
eventHolder.unbind("mouseup", onMouseUp);
|
||||||
|
eventHolder.unbind("dragstart", onDragStart);
|
||||||
|
eventHolder.unbind("drag", onDrag);
|
||||||
|
eventHolder.unbind("dragend", onDragEnd);
|
||||||
|
eventHolder.unbind("dblclick", onDblClick);
|
||||||
|
eventHolder.unbind("click", onClick);
|
||||||
|
|
||||||
|
if (panTimeout) clearTimeout(panTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOverlay(plot, ctx) {
|
||||||
|
if (panHint) {
|
||||||
|
ctx.strokeStyle = 'rgba(96, 160, 208, 0.7)';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
var startx = Math.round(panHint.start.x),
|
||||||
|
starty = Math.round(panHint.start.y),
|
||||||
|
endx, endy;
|
||||||
|
|
||||||
|
if (panAxes) {
|
||||||
|
if (panAxes[0].direction === 'x') {
|
||||||
|
endy = Math.round(panHint.start.y);
|
||||||
|
endx = Math.round(panHint.end.x);
|
||||||
|
} else if (panAxes[0].direction === 'y') {
|
||||||
|
endx = Math.round(panHint.start.x);
|
||||||
|
endy = Math.round(panHint.end.y);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
endx = Math.round(panHint.end.x);
|
||||||
|
endy = Math.round(panHint.end.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
if (panHint.end === false) {
|
||||||
|
ctx.moveTo(startx, starty - PANHINT_LENGTH_CONSTANT);
|
||||||
|
ctx.lineTo(startx, starty + PANHINT_LENGTH_CONSTANT);
|
||||||
|
|
||||||
|
ctx.moveTo(startx + PANHINT_LENGTH_CONSTANT, starty);
|
||||||
|
ctx.lineTo(startx - PANHINT_LENGTH_CONSTANT, starty);
|
||||||
|
} else {
|
||||||
|
var dirX = starty === endy;
|
||||||
|
|
||||||
|
ctx.moveTo(startx - (dirX ? 0 : PANHINT_LENGTH_CONSTANT), starty - (dirX ? PANHINT_LENGTH_CONSTANT : 0));
|
||||||
|
ctx.lineTo(startx + (dirX ? 0 : PANHINT_LENGTH_CONSTANT), starty + (dirX ? PANHINT_LENGTH_CONSTANT : 0));
|
||||||
|
|
||||||
|
ctx.moveTo(startx, starty);
|
||||||
|
ctx.lineTo(endx, endy);
|
||||||
|
|
||||||
|
ctx.moveTo(endx - (dirX ? 0 : PANHINT_LENGTH_CONSTANT), endy - (dirX ? PANHINT_LENGTH_CONSTANT : 0));
|
||||||
|
ctx.lineTo(endx + (dirX ? 0 : PANHINT_LENGTH_CONSTANT), endy + (dirX ? PANHINT_LENGTH_CONSTANT : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.getTouchedAxis = function(touchPointX, touchPointY) {
|
||||||
|
var ec = plot.getPlaceholder().offset();
|
||||||
|
ec.left = touchPointX - ec.left;
|
||||||
|
ec.top = touchPointY - ec.top;
|
||||||
|
|
||||||
|
var axis = plot.getXAxes().concat(plot.getYAxes()).filter(function (axis) {
|
||||||
|
var box = axis.box;
|
||||||
|
if (box !== undefined) {
|
||||||
|
return (ec.left > box.left) && (ec.left < box.left + box.width) &&
|
||||||
|
(ec.top > box.top) && (ec.top < box.top + box.height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(drawOverlay);
|
||||||
|
plot.hooks.bindEvents.push(bindEvents);
|
||||||
|
plot.hooks.shutdown.push(shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'navigate',
|
||||||
|
version: '1.3'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,786 @@
|
|||||||
|
/* Flot plugin for rendering pie charts.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The plugin assumes that each series has a single data value, and that each
|
||||||
|
value is a positive integer or zero. Negative numbers don't make sense for a
|
||||||
|
pie chart, and have unpredictable results. The values do NOT need to be
|
||||||
|
passed in as percentages; the plugin will calculate the total and per-slice
|
||||||
|
percentages internally.
|
||||||
|
|
||||||
|
* Created by Brian Medendorp
|
||||||
|
|
||||||
|
* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
|
||||||
|
series: {
|
||||||
|
pie: {
|
||||||
|
show: true/false
|
||||||
|
radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto'
|
||||||
|
innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect
|
||||||
|
startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result
|
||||||
|
tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show)
|
||||||
|
offset: {
|
||||||
|
top: integer value to move the pie up or down
|
||||||
|
left: integer value to move the pie left or right, or 'auto'
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF')
|
||||||
|
width: integer pixel width of the stroke
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: true/false, or 'auto'
|
||||||
|
formatter: a user-defined function that modifies the text/style of the label text
|
||||||
|
radius: 0-1 for percentage of fullsize, or a specified pixel length
|
||||||
|
background: {
|
||||||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000')
|
||||||
|
opacity: 0-1
|
||||||
|
},
|
||||||
|
threshold: 0-1 for the percentage value at which to hide labels (if they're too small)
|
||||||
|
},
|
||||||
|
combine: {
|
||||||
|
threshold: 0-1 for the percentage value at which to combine slices (if they're too small)
|
||||||
|
color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined
|
||||||
|
label: any text value of what the combined slice should be labeled
|
||||||
|
}
|
||||||
|
highlight: {
|
||||||
|
opacity: 0-1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
More detail and specific examples can be found in the included HTML file.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
// Maximum redraw attempts when fitting labels within the plot
|
||||||
|
|
||||||
|
var REDRAW_ATTEMPTS = 10;
|
||||||
|
|
||||||
|
// Factor by which to shrink the pie when fitting labels within the plot
|
||||||
|
|
||||||
|
var REDRAW_SHRINK = 0.95;
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
var canvas = null,
|
||||||
|
target = null,
|
||||||
|
options = null,
|
||||||
|
maxRadius = null,
|
||||||
|
centerLeft = null,
|
||||||
|
centerTop = null,
|
||||||
|
processed = false,
|
||||||
|
ctx = null;
|
||||||
|
|
||||||
|
// interactive variables
|
||||||
|
|
||||||
|
var highlights = [];
|
||||||
|
|
||||||
|
// add hook to determine if pie plugin in enabled, and then perform necessary operations
|
||||||
|
|
||||||
|
plot.hooks.processOptions.push(function(plot, options) {
|
||||||
|
if (options.series.pie.show) {
|
||||||
|
options.grid.show = false;
|
||||||
|
|
||||||
|
// set labels.show
|
||||||
|
|
||||||
|
if (options.series.pie.label.show === "auto") {
|
||||||
|
if (options.legend.show) {
|
||||||
|
options.series.pie.label.show = false;
|
||||||
|
} else {
|
||||||
|
options.series.pie.label.show = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set radius
|
||||||
|
|
||||||
|
if (options.series.pie.radius === "auto") {
|
||||||
|
if (options.series.pie.label.show) {
|
||||||
|
options.series.pie.radius = 3 / 4;
|
||||||
|
} else {
|
||||||
|
options.series.pie.radius = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure sane tilt
|
||||||
|
|
||||||
|
if (options.series.pie.tilt > 1) {
|
||||||
|
options.series.pie.tilt = 1;
|
||||||
|
} else if (options.series.pie.tilt < 0) {
|
||||||
|
options.series.pie.tilt = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function(plot, eventHolder) {
|
||||||
|
var options = plot.getOptions();
|
||||||
|
if (options.series.pie.show) {
|
||||||
|
if (options.grid.hoverable) {
|
||||||
|
eventHolder.unbind("mousemove").mousemove(onMouseMove);
|
||||||
|
}
|
||||||
|
if (options.grid.clickable) {
|
||||||
|
eventHolder.unbind("click").click(onClick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) {
|
||||||
|
var options = plot.getOptions();
|
||||||
|
if (options.series.pie.show) {
|
||||||
|
processDatapoints(plot, series, data, datapoints);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function(plot, octx) {
|
||||||
|
var options = plot.getOptions();
|
||||||
|
if (options.series.pie.show) {
|
||||||
|
drawOverlay(plot, octx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.draw.push(function(plot, newCtx) {
|
||||||
|
var options = plot.getOptions();
|
||||||
|
if (options.series.pie.show) {
|
||||||
|
draw(plot, newCtx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function processDatapoints(plot, series, datapoints) {
|
||||||
|
if (!processed) {
|
||||||
|
processed = true;
|
||||||
|
canvas = plot.getCanvas();
|
||||||
|
target = $(canvas).parent();
|
||||||
|
options = plot.getOptions();
|
||||||
|
plot.setData(combine(plot.getData()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function combine(data) {
|
||||||
|
var total = 0,
|
||||||
|
combined = 0,
|
||||||
|
numCombined = 0,
|
||||||
|
color = options.series.pie.combine.color,
|
||||||
|
newdata = [],
|
||||||
|
i,
|
||||||
|
value;
|
||||||
|
|
||||||
|
// Fix up the raw data from Flot, ensuring the data is numeric
|
||||||
|
|
||||||
|
for (i = 0; i < data.length; ++i) {
|
||||||
|
value = data[i].data;
|
||||||
|
|
||||||
|
// If the data is an array, we'll assume that it's a standard
|
||||||
|
// Flot x-y pair, and are concerned only with the second value.
|
||||||
|
|
||||||
|
// Note how we use the original array, rather than creating a
|
||||||
|
// new one; this is more efficient and preserves any extra data
|
||||||
|
// that the user may have stored in higher indexes.
|
||||||
|
|
||||||
|
if ($.isArray(value) && value.length === 1) {
|
||||||
|
value = value[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($.isArray(value)) {
|
||||||
|
// Equivalent to $.isNumeric() but compatible with jQuery < 1.7
|
||||||
|
if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) {
|
||||||
|
value[1] = +value[1];
|
||||||
|
} else {
|
||||||
|
value[1] = 0;
|
||||||
|
}
|
||||||
|
} else if (!isNaN(parseFloat(value)) && isFinite(value)) {
|
||||||
|
value = [1, +value];
|
||||||
|
} else {
|
||||||
|
value = [1, 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
data[i].data = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum up all the slices, so we can calculate percentages for each
|
||||||
|
|
||||||
|
for (i = 0; i < data.length; ++i) {
|
||||||
|
total += data[i].data[0][1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count the number of slices with percentages below the combine
|
||||||
|
// threshold; if it turns out to be just one, we won't combine.
|
||||||
|
|
||||||
|
for (i = 0; i < data.length; ++i) {
|
||||||
|
value = data[i].data[0][1];
|
||||||
|
if (value / total <= options.series.pie.combine.threshold) {
|
||||||
|
combined += value;
|
||||||
|
numCombined++;
|
||||||
|
if (!color) {
|
||||||
|
color = data[i].color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < data.length; ++i) {
|
||||||
|
value = data[i].data[0][1];
|
||||||
|
if (numCombined < 2 || value / total > options.series.pie.combine.threshold) {
|
||||||
|
newdata.push(
|
||||||
|
$.extend(data[i], { /* extend to allow keeping all other original data values
|
||||||
|
and using them e.g. in labelFormatter. */
|
||||||
|
data: [[1, value]],
|
||||||
|
color: data[i].color,
|
||||||
|
label: data[i].label,
|
||||||
|
angle: value * Math.PI * 2 / total,
|
||||||
|
percent: value / (total / 100)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numCombined > 1) {
|
||||||
|
newdata.push({
|
||||||
|
data: [[1, combined]],
|
||||||
|
color: color,
|
||||||
|
label: options.series.pie.combine.label,
|
||||||
|
angle: combined * Math.PI * 2 / total,
|
||||||
|
percent: combined / (total / 100)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return newdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw(plot, newCtx) {
|
||||||
|
if (!target) {
|
||||||
|
return; // if no series were passed
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvasWidth = plot.getPlaceholder().width(),
|
||||||
|
canvasHeight = plot.getPlaceholder().height(),
|
||||||
|
legendWidth = target.children().filter(".legend").children().width() || 0;
|
||||||
|
|
||||||
|
ctx = newCtx;
|
||||||
|
|
||||||
|
// WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE!
|
||||||
|
|
||||||
|
// When combining smaller slices into an 'other' slice, we need to
|
||||||
|
// add a new series. Since Flot gives plugins no way to modify the
|
||||||
|
// list of series, the pie plugin uses a hack where the first call
|
||||||
|
// to processDatapoints results in a call to setData with the new
|
||||||
|
// list of series, then subsequent processDatapoints do nothing.
|
||||||
|
|
||||||
|
// The plugin-global 'processed' flag is used to control this hack;
|
||||||
|
// it starts out false, and is set to true after the first call to
|
||||||
|
// processDatapoints.
|
||||||
|
|
||||||
|
// Unfortunately this turns future setData calls into no-ops; they
|
||||||
|
// call processDatapoints, the flag is true, and nothing happens.
|
||||||
|
|
||||||
|
// To fix this we'll set the flag back to false here in draw, when
|
||||||
|
// all series have been processed, so the next sequence of calls to
|
||||||
|
// processDatapoints once again starts out with a slice-combine.
|
||||||
|
// This is really a hack; in 0.9 we need to give plugins a proper
|
||||||
|
// way to modify series before any processing begins.
|
||||||
|
|
||||||
|
processed = false;
|
||||||
|
|
||||||
|
// calculate maximum radius and center point
|
||||||
|
maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2;
|
||||||
|
centerTop = canvasHeight / 2 + options.series.pie.offset.top;
|
||||||
|
centerLeft = canvasWidth / 2;
|
||||||
|
|
||||||
|
if (options.series.pie.offset.left === "auto") {
|
||||||
|
if (options.legend.position.match("w")) {
|
||||||
|
centerLeft += legendWidth / 2;
|
||||||
|
} else {
|
||||||
|
centerLeft -= legendWidth / 2;
|
||||||
|
}
|
||||||
|
if (centerLeft < maxRadius) {
|
||||||
|
centerLeft = maxRadius;
|
||||||
|
} else if (centerLeft > canvasWidth - maxRadius) {
|
||||||
|
centerLeft = canvasWidth - maxRadius;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
centerLeft += options.series.pie.offset.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
var slices = plot.getData(),
|
||||||
|
attempts = 0;
|
||||||
|
|
||||||
|
// Keep shrinking the pie's radius until drawPie returns true,
|
||||||
|
// indicating that all the labels fit, or we try too many times.
|
||||||
|
do {
|
||||||
|
if (attempts > 0) {
|
||||||
|
maxRadius *= REDRAW_SHRINK;
|
||||||
|
}
|
||||||
|
attempts += 1;
|
||||||
|
clear();
|
||||||
|
if (options.series.pie.tilt <= 0.8) {
|
||||||
|
drawShadow();
|
||||||
|
}
|
||||||
|
} while (!drawPie() && attempts < REDRAW_ATTEMPTS)
|
||||||
|
|
||||||
|
if (attempts >= REDRAW_ATTEMPTS) {
|
||||||
|
clear();
|
||||||
|
target.prepend("<div class='error'>Could not draw pie with labels contained inside canvas</div>");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plot.setSeries && plot.insertLegend) {
|
||||||
|
plot.setSeries(slices);
|
||||||
|
plot.insertLegend();
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're actually done at this point, just defining internal functions at this point
|
||||||
|
function clear() {
|
||||||
|
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||||
|
target.children().filter(".pieLabel, .pieLabelBackground").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawShadow() {
|
||||||
|
var shadowLeft = options.series.pie.shadow.left;
|
||||||
|
var shadowTop = options.series.pie.shadow.top;
|
||||||
|
var edge = 10;
|
||||||
|
var alpha = options.series.pie.shadow.alpha;
|
||||||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||||||
|
|
||||||
|
if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) {
|
||||||
|
return; // shadow would be outside canvas, so don't draw it
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(shadowLeft, shadowTop);
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.fillStyle = "#000";
|
||||||
|
|
||||||
|
// center and rotate to starting position
|
||||||
|
ctx.translate(centerLeft, centerTop);
|
||||||
|
ctx.scale(1, options.series.pie.tilt);
|
||||||
|
|
||||||
|
//radius -= edge;
|
||||||
|
for (var i = 1; i <= edge; i++) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(0, 0, radius, 0, Math.PI * 2, false);
|
||||||
|
ctx.fill();
|
||||||
|
radius -= i;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawPie() {
|
||||||
|
var startAngle = Math.PI * options.series.pie.startAngle;
|
||||||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||||||
|
var i;
|
||||||
|
// center and rotate to starting position
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(centerLeft, centerTop);
|
||||||
|
ctx.scale(1, options.series.pie.tilt);
|
||||||
|
//ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera
|
||||||
|
|
||||||
|
// draw slices
|
||||||
|
ctx.save();
|
||||||
|
|
||||||
|
var currentAngle = startAngle;
|
||||||
|
for (i = 0; i < slices.length; ++i) {
|
||||||
|
slices[i].startAngle = currentAngle;
|
||||||
|
drawSlice(slices[i].angle, slices[i].color, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// draw slice outlines
|
||||||
|
if (options.series.pie.stroke.width > 0) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.lineWidth = options.series.pie.stroke.width;
|
||||||
|
currentAngle = startAngle;
|
||||||
|
for (i = 0; i < slices.length; ++i) {
|
||||||
|
drawSlice(slices[i].angle, options.series.pie.stroke.color, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw donut hole
|
||||||
|
drawDonutHole(ctx);
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
|
||||||
|
// Draw the labels, returning true if they fit within the plot
|
||||||
|
if (options.series.pie.label.show) {
|
||||||
|
return drawLabels();
|
||||||
|
} else return true;
|
||||||
|
|
||||||
|
function drawSlice(angle, color, fill) {
|
||||||
|
if (angle <= 0 || isNaN(angle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
ctx.fillStyle = color;
|
||||||
|
} else {
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
if (Math.abs(angle - Math.PI * 2) > 0.000000001) {
|
||||||
|
ctx.moveTo(0, 0); // Center of the pie
|
||||||
|
}
|
||||||
|
|
||||||
|
//ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera
|
||||||
|
ctx.arc(0, 0, radius, currentAngle, currentAngle + angle / 2, false);
|
||||||
|
ctx.arc(0, 0, radius, currentAngle + angle / 2, currentAngle + angle, false);
|
||||||
|
ctx.closePath();
|
||||||
|
//ctx.rotate(angle); // This doesn't work properly in Opera
|
||||||
|
currentAngle += angle;
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
ctx.fill();
|
||||||
|
} else {
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawLabels() {
|
||||||
|
var currentAngle = startAngle;
|
||||||
|
var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius;
|
||||||
|
|
||||||
|
for (var i = 0; i < slices.length; ++i) {
|
||||||
|
if (slices[i].percent >= options.series.pie.label.threshold * 100) {
|
||||||
|
if (!drawLabel(slices[i], currentAngle, i)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentAngle += slices[i].angle;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
function drawLabel(slice, startAngle, index) {
|
||||||
|
if (slice.data[0][1] === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// format label text
|
||||||
|
var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter;
|
||||||
|
|
||||||
|
if (lf) {
|
||||||
|
text = lf(slice.label, slice);
|
||||||
|
} else {
|
||||||
|
text = slice.label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plf) {
|
||||||
|
text = plf(text, slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
var halfAngle = ((startAngle + slice.angle) + startAngle) / 2;
|
||||||
|
var x = centerLeft + Math.round(Math.cos(halfAngle) * radius);
|
||||||
|
var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt;
|
||||||
|
|
||||||
|
var html = "<span class='pieLabel' id='pieLabel" + index + "' style='position:absolute;top:" + y + "px;left:" + x + "px;'>" + text + "</span>";
|
||||||
|
target.append(html);
|
||||||
|
|
||||||
|
var label = target.children("#pieLabel" + index);
|
||||||
|
var labelTop = (y - label.height() / 2);
|
||||||
|
var labelLeft = (x - label.width() / 2);
|
||||||
|
|
||||||
|
label.css("top", labelTop);
|
||||||
|
label.css("left", labelLeft);
|
||||||
|
|
||||||
|
// check to make sure that the label is not outside the canvas
|
||||||
|
if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.series.pie.label.background.opacity !== 0) {
|
||||||
|
// put in the transparent background separately to avoid blended labels and label boxes
|
||||||
|
var c = options.series.pie.label.background.color;
|
||||||
|
if (c == null) {
|
||||||
|
c = slice.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;";
|
||||||
|
$("<div class='pieLabelBackground' style='position:absolute;width:" + label.width() + "px;height:" + label.height() + "px;" + pos + "background-color:" + c + ";'></div>")
|
||||||
|
.css("opacity", options.series.pie.label.background.opacity)
|
||||||
|
.insertBefore(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} // end individual label function
|
||||||
|
} // end drawLabels function
|
||||||
|
} // end drawPie function
|
||||||
|
} // end draw function
|
||||||
|
|
||||||
|
// Placed here because it needs to be accessed from multiple locations
|
||||||
|
|
||||||
|
function drawDonutHole(layer) {
|
||||||
|
if (options.series.pie.innerRadius > 0) {
|
||||||
|
// subtract the center
|
||||||
|
layer.save();
|
||||||
|
var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius;
|
||||||
|
layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color
|
||||||
|
layer.beginPath();
|
||||||
|
layer.fillStyle = options.series.pie.stroke.color;
|
||||||
|
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
|
||||||
|
layer.fill();
|
||||||
|
layer.closePath();
|
||||||
|
layer.restore();
|
||||||
|
|
||||||
|
// add inner stroke
|
||||||
|
layer.save();
|
||||||
|
layer.beginPath();
|
||||||
|
layer.strokeStyle = options.series.pie.stroke.color;
|
||||||
|
layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false);
|
||||||
|
layer.stroke();
|
||||||
|
layer.closePath();
|
||||||
|
layer.restore();
|
||||||
|
|
||||||
|
// TODO: add extra shadow inside hole (with a mask) if the pie is tilted.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//-- Additional Interactive related functions --
|
||||||
|
|
||||||
|
function isPointInPoly(poly, pt) {
|
||||||
|
for (var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) {
|
||||||
|
((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) ||
|
||||||
|
(poly[j][1] <= pt[1] && pt[1] < poly[i][1])) &&
|
||||||
|
(pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) &&
|
||||||
|
(c = !c);
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNearbySlice(mouseX, mouseY) {
|
||||||
|
var slices = plot.getData(),
|
||||||
|
options = plot.getOptions(),
|
||||||
|
radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius,
|
||||||
|
x, y;
|
||||||
|
|
||||||
|
for (var i = 0; i < slices.length; ++i) {
|
||||||
|
var s = slices[i];
|
||||||
|
if (s.pie.show) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, 0); // Center of the pie
|
||||||
|
//ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here.
|
||||||
|
ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false);
|
||||||
|
ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false);
|
||||||
|
ctx.closePath();
|
||||||
|
x = mouseX - centerLeft;
|
||||||
|
y = mouseY - centerTop;
|
||||||
|
|
||||||
|
if (ctx.isPointInPath) {
|
||||||
|
if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) {
|
||||||
|
ctx.restore();
|
||||||
|
return {
|
||||||
|
datapoint: [s.percent, s.data],
|
||||||
|
dataIndex: 0,
|
||||||
|
series: s,
|
||||||
|
seriesIndex: i
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// excanvas for IE doesn;t support isPointInPath, this is a workaround.
|
||||||
|
var p1X = radius * Math.cos(s.startAngle),
|
||||||
|
p1Y = radius * Math.sin(s.startAngle),
|
||||||
|
p2X = radius * Math.cos(s.startAngle + s.angle / 4),
|
||||||
|
p2Y = radius * Math.sin(s.startAngle + s.angle / 4),
|
||||||
|
p3X = radius * Math.cos(s.startAngle + s.angle / 2),
|
||||||
|
p3Y = radius * Math.sin(s.startAngle + s.angle / 2),
|
||||||
|
p4X = radius * Math.cos(s.startAngle + s.angle / 1.5),
|
||||||
|
p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5),
|
||||||
|
p5X = radius * Math.cos(s.startAngle + s.angle),
|
||||||
|
p5Y = radius * Math.sin(s.startAngle + s.angle),
|
||||||
|
arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]],
|
||||||
|
arrPoint = [x, y];
|
||||||
|
|
||||||
|
// TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt?
|
||||||
|
|
||||||
|
if (isPointInPoly(arrPoly, arrPoint)) {
|
||||||
|
ctx.restore();
|
||||||
|
return {
|
||||||
|
datapoint: [s.percent, s.data],
|
||||||
|
dataIndex: 0,
|
||||||
|
series: s,
|
||||||
|
seriesIndex: i
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
triggerClickHoverEvent("plothover", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(e) {
|
||||||
|
triggerClickHoverEvent("plotclick", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger click or hover event (they send the same parameters so we share their code)
|
||||||
|
|
||||||
|
function triggerClickHoverEvent(eventname, e) {
|
||||||
|
var offset = plot.offset();
|
||||||
|
var canvasX = parseInt(e.pageX - offset.left);
|
||||||
|
var canvasY = parseInt(e.pageY - offset.top);
|
||||||
|
var item = findNearbySlice(canvasX, canvasY);
|
||||||
|
|
||||||
|
if (options.grid.autoHighlight) {
|
||||||
|
// clear auto-highlights
|
||||||
|
for (var i = 0; i < highlights.length; ++i) {
|
||||||
|
var h = highlights[i];
|
||||||
|
if (h.auto === eventname && !(item && h.series === item.series)) {
|
||||||
|
unhighlight(h.series);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlight the slice
|
||||||
|
|
||||||
|
if (item) {
|
||||||
|
highlight(item.series, eventname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// trigger any hover bind events
|
||||||
|
|
||||||
|
var pos = { pageX: e.pageX, pageY: e.pageY };
|
||||||
|
target.trigger(eventname, [pos, item]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlight(s, auto) {
|
||||||
|
//if (typeof s == "number") {
|
||||||
|
// s = series[s];
|
||||||
|
//}
|
||||||
|
|
||||||
|
var i = indexOfHighlight(s);
|
||||||
|
|
||||||
|
if (i === -1) {
|
||||||
|
highlights.push({ series: s, auto: auto });
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
} else if (!auto) {
|
||||||
|
highlights[i].auto = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unhighlight(s) {
|
||||||
|
if (s == null) {
|
||||||
|
highlights = [];
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
//if (typeof s == "number") {
|
||||||
|
// s = series[s];
|
||||||
|
//}
|
||||||
|
|
||||||
|
var i = indexOfHighlight(s);
|
||||||
|
|
||||||
|
if (i !== -1) {
|
||||||
|
highlights.splice(i, 1);
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexOfHighlight(s) {
|
||||||
|
for (var i = 0; i < highlights.length; ++i) {
|
||||||
|
var h = highlights[i];
|
||||||
|
if (h.series === s) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawOverlay(plot, octx) {
|
||||||
|
var options = plot.getOptions();
|
||||||
|
var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius;
|
||||||
|
|
||||||
|
octx.save();
|
||||||
|
octx.translate(centerLeft, centerTop);
|
||||||
|
octx.scale(1, options.series.pie.tilt);
|
||||||
|
|
||||||
|
for (var i = 0; i < highlights.length; ++i) {
|
||||||
|
drawHighlight(highlights[i].series);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawDonutHole(octx);
|
||||||
|
|
||||||
|
octx.restore();
|
||||||
|
|
||||||
|
function drawHighlight(series) {
|
||||||
|
if (series.angle <= 0 || isNaN(series.angle)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString();
|
||||||
|
octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor
|
||||||
|
octx.beginPath();
|
||||||
|
if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) {
|
||||||
|
octx.moveTo(0, 0); // Center of the pie
|
||||||
|
}
|
||||||
|
octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false);
|
||||||
|
octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false);
|
||||||
|
octx.closePath();
|
||||||
|
octx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // end init (plugin body)
|
||||||
|
|
||||||
|
// define pie specific options and their default values
|
||||||
|
var options = {
|
||||||
|
series: {
|
||||||
|
pie: {
|
||||||
|
show: false,
|
||||||
|
radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value)
|
||||||
|
innerRadius: 0, /* for donut */
|
||||||
|
startAngle: 3 / 2,
|
||||||
|
tilt: 1,
|
||||||
|
shadow: {
|
||||||
|
left: 5, // shadow left offset
|
||||||
|
top: 15, // shadow top offset
|
||||||
|
alpha: 0.02 // shadow alpha
|
||||||
|
},
|
||||||
|
offset: {
|
||||||
|
top: 0,
|
||||||
|
left: "auto"
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
color: "#fff",
|
||||||
|
width: 1
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
show: "auto",
|
||||||
|
formatter: function(label, slice) {
|
||||||
|
return "<div style='font-size:x-small;text-align:center;padding:2px;color:" + slice.color + ";'>" + label + "<br/>" + Math.round(slice.percent) + "%</div>";
|
||||||
|
}, // formatter function
|
||||||
|
radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value)
|
||||||
|
background: {
|
||||||
|
color: null,
|
||||||
|
opacity: 0
|
||||||
|
},
|
||||||
|
threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow)
|
||||||
|
},
|
||||||
|
combine: {
|
||||||
|
threshold: -1, // percentage at which to combine little slices into one larger slice
|
||||||
|
color: null, // color to give the new slice (auto-generated if null)
|
||||||
|
label: "Other" // label to give the new slice
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
//color: "#fff", // will add this functionality once parseColor is available
|
||||||
|
opacity: 0.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: "pie",
|
||||||
|
version: "1.1"
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* Flot plugin for automatically redrawing plots as the placeholder resizes.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
It works by listening for changes on the placeholder div (through the jQuery
|
||||||
|
resize event plugin) - if the size changes, it will redraw the plot.
|
||||||
|
|
||||||
|
There are no options. If you need to disable the plugin for some plots, you
|
||||||
|
can just fix the size of their placeholders.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Inline dependency:
|
||||||
|
* jQuery resize event - v1.1 - 3/14/2010
|
||||||
|
* http://benalman.com/projects/jquery-resize-plugin/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||||||
|
* Dual licensed under the MIT and GPL licenses.
|
||||||
|
* http://benalman.com/about/license/
|
||||||
|
*/
|
||||||
|
(function($,e,t){"$:nomunge";var i=[],n=$.resize=$.extend($.resize,{}),a,r=false,s="setTimeout",u="resize",m=u+"-special-event",o="pendingDelay",l="activeDelay",f="throttleWindow";n[o]=200;n[l]=20;n[f]=true;$.event.special[u]={setup:function(){if(!n[f]&&this[s]){return false}var e=$(this);i.push(this);e.data(m,{w:e.width(),h:e.height()});if(i.length===1){a=t;h()}},teardown:function(){if(!n[f]&&this[s]){return false}var e=$(this);for(var t=i.length-1;t>=0;t--){if(i[t]==this){i.splice(t,1);break}}e.removeData(m);if(!i.length){if(r){cancelAnimationFrame(a)}else{clearTimeout(a)}a=null}},add:function(e){if(!n[f]&&this[s]){return false}var i;function a(e,n,a){var r=$(this),s=r.data(m)||{};s.w=n!==t?n:r.width();s.h=a!==t?a:r.height();i.apply(this,arguments)}if($.isFunction(e)){i=e;return a}else{i=e.handler;e.handler=a}}};function h(t){if(r===true){r=t||1}for(var s=i.length-1;s>=0;s--){var l=$(i[s]);if(l[0]==e||l.is(":visible")){var f=l.width(),c=l.height(),d=l.data(m);if(d&&(f!==d.w||c!==d.h)){l.trigger(u,[d.w=f,d.h=c]);r=t||true}}else{d=l.data(m);d.w=0;d.h=0}}if(a!==null){if(r&&(t==null||t-r<1e3)){a=e.requestAnimationFrame(h)}else{a=setTimeout(h,n[o]);r=false}}}if(!e.requestAnimationFrame){e.requestAnimationFrame=function(){return e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(t,i){return e.setTimeout(function(){t((new Date).getTime())},n[l])}}()}if(!e.cancelAnimationFrame){e.cancelAnimationFrame=function(){return e.webkitCancelRequestAnimationFrame||e.mozCancelRequestAnimationFrame||e.oCancelRequestAnimationFrame||e.msCancelRequestAnimationFrame||clearTimeout}()}})(jQuery,this);
|
||||||
|
|
||||||
|
/* eslint-enable */
|
||||||
|
(function ($) {
|
||||||
|
var options = { }; // no options
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function onResize() {
|
||||||
|
var placeholder = plot.getPlaceholder();
|
||||||
|
|
||||||
|
// somebody might have hidden us and we can't plot
|
||||||
|
// when we don't have the dimensions
|
||||||
|
if (placeholder.width() === 0 || placeholder.height() === 0) return;
|
||||||
|
|
||||||
|
plot.resize();
|
||||||
|
plot.setupGrid();
|
||||||
|
plot.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents(plot, eventHolder) {
|
||||||
|
plot.getPlaceholder().resize(onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(plot, eventHolder) {
|
||||||
|
plot.getPlaceholder().unbind("resize", onResize);
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(bindEvents);
|
||||||
|
plot.hooks.shutdown.push(shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'resize',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
var saturated = {
|
||||||
|
saturate: function (a) {
|
||||||
|
if (a === Infinity) {
|
||||||
|
return Number.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a === -Infinity) {
|
||||||
|
return -Number.MAX_VALUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a;
|
||||||
|
},
|
||||||
|
delta: function(min, max, noTicks) {
|
||||||
|
return ((max - min) / noTicks) === Infinity ? (max / noTicks - min / noTicks) : (max - min) / noTicks
|
||||||
|
},
|
||||||
|
multiply: function (a, b) {
|
||||||
|
return saturated.saturate(a * b);
|
||||||
|
},
|
||||||
|
// returns c * bInt * a. Beahves properly in the case where c is negative
|
||||||
|
// and bInt * a is bigger that Number.MAX_VALUE (Infinity)
|
||||||
|
multiplyAdd: function (a, bInt, c) {
|
||||||
|
if (isFinite(a * bInt)) {
|
||||||
|
return saturated.saturate(a * bInt + c);
|
||||||
|
} else {
|
||||||
|
var result = c;
|
||||||
|
|
||||||
|
for (var i = 0; i < bInt; i++) {
|
||||||
|
result += a;
|
||||||
|
}
|
||||||
|
|
||||||
|
return saturated.saturate(result);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// round to nearby lower multiple of base
|
||||||
|
floorInBase: function(n, base) {
|
||||||
|
return base * Math.floor(n / base);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.plot.saturated = saturated;
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,517 @@
|
|||||||
|
/* Flot plugin for selecting regions of a plot.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
|
||||||
|
selection: {
|
||||||
|
mode: null or "x" or "y" or "xy" or "smart",
|
||||||
|
color: color,
|
||||||
|
shape: "round" or "miter" or "bevel",
|
||||||
|
visualization: "fill" or "focus",
|
||||||
|
minSize: number of pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
Selection support is enabled by setting the mode to one of "x", "y" or "xy".
|
||||||
|
In "x" mode, the user will only be able to specify the x range, similarly for
|
||||||
|
"y" mode. For "xy", the selection becomes a rectangle where both ranges can be
|
||||||
|
specified. "color" is color of the selection (if you need to change the color
|
||||||
|
later on, you can get to it with plot.getOptions().selection.color). "shape"
|
||||||
|
is the shape of the corners of the selection.
|
||||||
|
|
||||||
|
The way how the selection is visualized, can be changed by using the option
|
||||||
|
"visualization". Flot currently supports two modes: "focus" and "fill". The
|
||||||
|
option "focus" draws a colored bezel around the selected area while keeping
|
||||||
|
the selected area clear. The option "fill" highlights (i.e., fills) the
|
||||||
|
selected area with a colored highlight.
|
||||||
|
|
||||||
|
"minSize" is the minimum size a selection can be in pixels. This value can
|
||||||
|
be customized to determine the smallest size a selection can be and still
|
||||||
|
have the selection rectangle be displayed. When customizing this value, the
|
||||||
|
fact that it refers to pixels, not axis units must be taken into account.
|
||||||
|
Thus, for example, if there is a bar graph in time mode with BarWidth set to 1
|
||||||
|
minute, setting "minSize" to 1 will not make the minimum selection size 1
|
||||||
|
minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent
|
||||||
|
"plotunselected" events from being fired when the user clicks the mouse without
|
||||||
|
dragging.
|
||||||
|
|
||||||
|
When selection support is enabled, a "plotselected" event will be emitted on
|
||||||
|
the DOM element you passed into the plot function. The event handler gets a
|
||||||
|
parameter with the ranges selected on the axes, like this:
|
||||||
|
|
||||||
|
placeholder.bind( "plotselected", function( event, ranges ) {
|
||||||
|
alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
|
||||||
|
// similar for yaxis - with multiple axes, the extra ones are in
|
||||||
|
// x2axis, x3axis, ...
|
||||||
|
});
|
||||||
|
|
||||||
|
The "plotselected" event is only fired when the user has finished making the
|
||||||
|
selection. A "plotselecting" event is fired during the process with the same
|
||||||
|
parameters as the "plotselected" event, in case you want to know what's
|
||||||
|
happening while it's happening,
|
||||||
|
|
||||||
|
A "plotunselected" event with no arguments is emitted when the user clicks the
|
||||||
|
mouse to remove the selection. As stated above, setting "minSize" to 0 will
|
||||||
|
destroy this behavior.
|
||||||
|
|
||||||
|
The plugin allso adds the following methods to the plot object:
|
||||||
|
|
||||||
|
- setSelection( ranges, preventEvent )
|
||||||
|
|
||||||
|
Set the selection rectangle. The passed in ranges is on the same form as
|
||||||
|
returned in the "plotselected" event. If the selection mode is "x", you
|
||||||
|
should put in either an xaxis range, if the mode is "y" you need to put in
|
||||||
|
an yaxis range and both xaxis and yaxis if the selection mode is "xy", like
|
||||||
|
this:
|
||||||
|
|
||||||
|
setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
|
||||||
|
|
||||||
|
setSelection will trigger the "plotselected" event when called. If you don't
|
||||||
|
want that to happen, e.g. if you're inside a "plotselected" handler, pass
|
||||||
|
true as the second parameter. If you are using multiple axes, you can
|
||||||
|
specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of
|
||||||
|
xaxis, the plugin picks the first one it sees.
|
||||||
|
|
||||||
|
- clearSelection( preventEvent )
|
||||||
|
|
||||||
|
Clear the selection rectangle. Pass in true to avoid getting a
|
||||||
|
"plotunselected" event.
|
||||||
|
|
||||||
|
- getSelection()
|
||||||
|
|
||||||
|
Returns the current selection in the same format as the "plotselected"
|
||||||
|
event. If there's currently no selection, the function returns null.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
function init(plot) {
|
||||||
|
var selection = {
|
||||||
|
first: {x: -1, y: -1},
|
||||||
|
second: {x: -1, y: -1},
|
||||||
|
show: false,
|
||||||
|
currentMode: 'xy',
|
||||||
|
active: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var SNAPPING_CONSTANT = $.plot.uiConstants.SNAPPING_CONSTANT;
|
||||||
|
|
||||||
|
// FIXME: The drag handling implemented here should be
|
||||||
|
// abstracted out, there's some similar code from a library in
|
||||||
|
// the navigation plugin, this should be massaged a bit to fit
|
||||||
|
// the Flot cases here better and reused. Doing this would
|
||||||
|
// make this plugin much slimmer.
|
||||||
|
var savedhandlers = {};
|
||||||
|
|
||||||
|
function onDrag(e) {
|
||||||
|
if (selection.active) {
|
||||||
|
updateSelection(e);
|
||||||
|
|
||||||
|
plot.getPlaceholder().trigger("plotselecting", [ getSelection() ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragStart(e) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
// only accept left-click
|
||||||
|
if (e.which !== 1 || o.selection.mode === null) return;
|
||||||
|
|
||||||
|
// reinitialize currentMode
|
||||||
|
selection.currentMode = 'xy';
|
||||||
|
|
||||||
|
// cancel out any text selections
|
||||||
|
document.body.focus();
|
||||||
|
|
||||||
|
// prevent text selection and drag in old-school browsers
|
||||||
|
if (document.onselectstart !== undefined && savedhandlers.onselectstart == null) {
|
||||||
|
savedhandlers.onselectstart = document.onselectstart;
|
||||||
|
document.onselectstart = function () { return false; };
|
||||||
|
}
|
||||||
|
if (document.ondrag !== undefined && savedhandlers.ondrag == null) {
|
||||||
|
savedhandlers.ondrag = document.ondrag;
|
||||||
|
document.ondrag = function () { return false; };
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectionPos(selection.first, e);
|
||||||
|
|
||||||
|
selection.active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd(e) {
|
||||||
|
// revert drag stuff for old-school browsers
|
||||||
|
if (document.onselectstart !== undefined) {
|
||||||
|
document.onselectstart = savedhandlers.onselectstart;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.ondrag !== undefined) {
|
||||||
|
document.ondrag = savedhandlers.ondrag;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no more dragging
|
||||||
|
selection.active = false;
|
||||||
|
updateSelection(e);
|
||||||
|
|
||||||
|
if (selectionIsSane()) {
|
||||||
|
triggerSelectedEvent();
|
||||||
|
} else {
|
||||||
|
// this counts as a clear
|
||||||
|
plot.getPlaceholder().trigger("plotunselected", [ ]);
|
||||||
|
plot.getPlaceholder().trigger("plotselecting", [ null ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelection() {
|
||||||
|
if (!selectionIsSane()) return null;
|
||||||
|
|
||||||
|
if (!selection.show) return null;
|
||||||
|
|
||||||
|
var r = {},
|
||||||
|
c1 = {x: selection.first.x, y: selection.first.y},
|
||||||
|
c2 = {x: selection.second.x, y: selection.second.y};
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === 'x') {
|
||||||
|
c1.y = 0;
|
||||||
|
c2.y = plot.height();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === 'y') {
|
||||||
|
c1.x = 0;
|
||||||
|
c2.x = plot.width();
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each(plot.getAxes(), function (name, axis) {
|
||||||
|
if (axis.used) {
|
||||||
|
var p1 = axis.c2p(c1[axis.direction]), p2 = axis.c2p(c2[axis.direction]);
|
||||||
|
r[name] = { from: Math.min(p1, p2), to: Math.max(p1, p2) };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSelectedEvent() {
|
||||||
|
var r = getSelection();
|
||||||
|
|
||||||
|
plot.getPlaceholder().trigger("plotselected", [ r ]);
|
||||||
|
|
||||||
|
// backwards-compat stuff, to be removed in future
|
||||||
|
if (r.xaxis && r.yaxis) {
|
||||||
|
plot.getPlaceholder().trigger("selected", [ { x1: r.xaxis.from, y1: r.yaxis.from, x2: r.xaxis.to, y2: r.yaxis.to } ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(min, value, max) {
|
||||||
|
return value < min ? min : (value > max ? max : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionDirection(plot) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
if (o.selection.mode === 'smart') {
|
||||||
|
return selection.currentMode;
|
||||||
|
} else {
|
||||||
|
return o.selection.mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMode(pos) {
|
||||||
|
if (selection.first) {
|
||||||
|
var delta = {
|
||||||
|
x: pos.x - selection.first.x,
|
||||||
|
y: pos.y - selection.first.y
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Math.abs(delta.x) < SNAPPING_CONSTANT) {
|
||||||
|
selection.currentMode = 'y';
|
||||||
|
} else if (Math.abs(delta.y) < SNAPPING_CONSTANT) {
|
||||||
|
selection.currentMode = 'x';
|
||||||
|
} else {
|
||||||
|
selection.currentMode = 'xy';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelectionPos(pos, e) {
|
||||||
|
var offset = plot.getPlaceholder().offset();
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
pos.x = clamp(0, e.pageX - offset.left - plotOffset.left, plot.width());
|
||||||
|
pos.y = clamp(0, e.pageY - offset.top - plotOffset.top, plot.height());
|
||||||
|
|
||||||
|
if (pos !== selection.first) updateMode(pos);
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === "y") {
|
||||||
|
pos.x = pos === selection.first ? 0 : plot.width();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === "x") {
|
||||||
|
pos.y = pos === selection.first ? 0 : plot.height();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSelection(pos) {
|
||||||
|
if (pos.pageX == null) return;
|
||||||
|
|
||||||
|
setSelectionPos(selection.second, pos);
|
||||||
|
if (selectionIsSane()) {
|
||||||
|
selection.show = true;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
} else clearSelection(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection(preventEvent) {
|
||||||
|
if (selection.show) {
|
||||||
|
selection.show = false;
|
||||||
|
selection.currentMode = '';
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
if (!preventEvent) {
|
||||||
|
plot.getPlaceholder().trigger("plotunselected", [ ]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// function taken from markings support in Flot
|
||||||
|
function extractRange(ranges, coord) {
|
||||||
|
var axis, from, to, key, axes = plot.getAxes();
|
||||||
|
|
||||||
|
for (var k in axes) {
|
||||||
|
axis = axes[k];
|
||||||
|
if (axis.direction === coord) {
|
||||||
|
key = coord + axis.n + "axis";
|
||||||
|
if (!ranges[key] && axis.n === 1) {
|
||||||
|
// support x1axis as xaxis
|
||||||
|
key = coord + "axis";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ranges[key]) {
|
||||||
|
from = ranges[key].from;
|
||||||
|
to = ranges[key].to;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// backwards-compat stuff - to be removed in future
|
||||||
|
if (!ranges[key]) {
|
||||||
|
axis = coord === "x" ? plot.getXAxes()[0] : plot.getYAxes()[0];
|
||||||
|
from = ranges[coord + "1"];
|
||||||
|
to = ranges[coord + "2"];
|
||||||
|
}
|
||||||
|
|
||||||
|
// auto-reverse as an added bonus
|
||||||
|
if (from != null && to != null && from > to) {
|
||||||
|
var tmp = from;
|
||||||
|
from = to;
|
||||||
|
to = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { from: from, to: to, axis: axis };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSelection(ranges, preventEvent) {
|
||||||
|
var range;
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === "y") {
|
||||||
|
selection.first.x = 0;
|
||||||
|
selection.second.x = plot.width();
|
||||||
|
} else {
|
||||||
|
range = extractRange(ranges, "x");
|
||||||
|
selection.first.x = range.axis.p2c(range.from);
|
||||||
|
selection.second.x = range.axis.p2c(range.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === "x") {
|
||||||
|
selection.first.y = 0;
|
||||||
|
selection.second.y = plot.height();
|
||||||
|
} else {
|
||||||
|
range = extractRange(ranges, "y");
|
||||||
|
selection.first.y = range.axis.p2c(range.from);
|
||||||
|
selection.second.y = range.axis.p2c(range.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
selection.show = true;
|
||||||
|
plot.triggerRedrawOverlay();
|
||||||
|
if (!preventEvent && selectionIsSane()) {
|
||||||
|
triggerSelectedEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionIsSane() {
|
||||||
|
var minSize = plot.getOptions().selection.minSize;
|
||||||
|
return Math.abs(selection.second.x - selection.first.x) >= minSize &&
|
||||||
|
Math.abs(selection.second.y - selection.first.y) >= minSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.clearSelection = clearSelection;
|
||||||
|
plot.setSelection = setSelection;
|
||||||
|
plot.getSelection = getSelection;
|
||||||
|
|
||||||
|
plot.hooks.bindEvents.push(function(plot, eventHolder) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
if (o.selection.mode != null) {
|
||||||
|
plot.addEventHandler("dragstart", onDragStart, eventHolder, 0);
|
||||||
|
plot.addEventHandler("drag", onDrag, eventHolder, 0);
|
||||||
|
plot.addEventHandler("dragend", onDragEnd, eventHolder, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function drawSelectionDecorations(ctx, x, y, w, h, oX, oY, mode) {
|
||||||
|
var spacing = 3;
|
||||||
|
var fullEarWidth = 15;
|
||||||
|
var earWidth = Math.max(0, Math.min(fullEarWidth, w / 2 - 2, h / 2 - 2));
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
|
||||||
|
if (mode === 'xy') {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y + earWidth);
|
||||||
|
ctx.lineTo(x - 3, y + earWidth);
|
||||||
|
ctx.lineTo(x - 3, y - 3);
|
||||||
|
ctx.lineTo(x + earWidth, y - 3);
|
||||||
|
ctx.lineTo(x + earWidth, y);
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.moveTo(x, y + h - earWidth);
|
||||||
|
ctx.lineTo(x - 3, y + h - earWidth);
|
||||||
|
ctx.lineTo(x - 3, y + h + 3);
|
||||||
|
ctx.lineTo(x + earWidth, y + h + 3);
|
||||||
|
ctx.lineTo(x + earWidth, y + h);
|
||||||
|
ctx.lineTo(x, y + h);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.moveTo(x + w, y + earWidth);
|
||||||
|
ctx.lineTo(x + w + 3, y + earWidth);
|
||||||
|
ctx.lineTo(x + w + 3, y - 3);
|
||||||
|
ctx.lineTo(x + w - earWidth, y - 3);
|
||||||
|
ctx.lineTo(x + w - earWidth, y);
|
||||||
|
ctx.lineTo(x + w, y);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.moveTo(x + w, y + h - earWidth);
|
||||||
|
ctx.lineTo(x + w + 3, y + h - earWidth);
|
||||||
|
ctx.lineTo(x + w + 3, y + h + 3);
|
||||||
|
ctx.lineTo(x + w - earWidth, y + h + 3);
|
||||||
|
ctx.lineTo(x + w - earWidth, y + h);
|
||||||
|
ctx.lineTo(x + w, y + h);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
x = oX;
|
||||||
|
y = oY;
|
||||||
|
|
||||||
|
if (mode === 'x') {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, y + fullEarWidth);
|
||||||
|
ctx.lineTo(x, y - fullEarWidth);
|
||||||
|
ctx.lineTo(x - spacing, y - fullEarWidth);
|
||||||
|
ctx.lineTo(x - spacing, y + fullEarWidth);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.moveTo(x + w, y + fullEarWidth);
|
||||||
|
ctx.lineTo(x + w, y - fullEarWidth);
|
||||||
|
ctx.lineTo(x + w + spacing, y - fullEarWidth);
|
||||||
|
ctx.lineTo(x + w + spacing, y + fullEarWidth);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode === 'y') {
|
||||||
|
ctx.beginPath();
|
||||||
|
|
||||||
|
ctx.moveTo(x - fullEarWidth, y);
|
||||||
|
ctx.lineTo(x + fullEarWidth, y);
|
||||||
|
ctx.lineTo(x + fullEarWidth, y - spacing);
|
||||||
|
ctx.lineTo(x - fullEarWidth, y - spacing);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.moveTo(x - fullEarWidth, y + h);
|
||||||
|
ctx.lineTo(x + fullEarWidth, y + h);
|
||||||
|
ctx.lineTo(x + fullEarWidth, y + h + spacing);
|
||||||
|
ctx.lineTo(x - fullEarWidth, y + h + spacing);
|
||||||
|
ctx.closePath();
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.drawOverlay.push(function (plot, ctx) {
|
||||||
|
// draw selection
|
||||||
|
if (selection.show && selectionIsSane()) {
|
||||||
|
var plotOffset = plot.getPlotOffset();
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(plotOffset.left, plotOffset.top);
|
||||||
|
|
||||||
|
var c = $.color.parse(o.selection.color);
|
||||||
|
var visualization = o.selection.visualization;
|
||||||
|
|
||||||
|
var scalingFactor = 1;
|
||||||
|
|
||||||
|
// use a dimmer scaling factor if visualization is "fill"
|
||||||
|
if (visualization === "fill") {
|
||||||
|
scalingFactor = 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.strokeStyle = c.scale('a', scalingFactor).toString();
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.lineJoin = o.selection.shape;
|
||||||
|
ctx.fillStyle = c.scale('a', 0.4).toString();
|
||||||
|
|
||||||
|
var x = Math.min(selection.first.x, selection.second.x) + 0.5,
|
||||||
|
oX = x,
|
||||||
|
y = Math.min(selection.first.y, selection.second.y) + 0.5,
|
||||||
|
oY = y,
|
||||||
|
w = Math.abs(selection.second.x - selection.first.x) - 1,
|
||||||
|
h = Math.abs(selection.second.y - selection.first.y) - 1;
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === 'x') {
|
||||||
|
h += y;
|
||||||
|
y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectionDirection(plot) === 'y') {
|
||||||
|
w += x;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (visualization === "fill") {
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.strokeRect(x, y, w, h);
|
||||||
|
} else {
|
||||||
|
ctx.fillRect(0, 0, plot.width(), plot.height());
|
||||||
|
ctx.clearRect(x, y, w, h);
|
||||||
|
drawSelectionDecorations(ctx, x, y, w, h, oX, oY, selectionDirection(plot));
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
plot.hooks.shutdown.push(function (plot, eventHolder) {
|
||||||
|
eventHolder.unbind("dragstart", onDragStart);
|
||||||
|
eventHolder.unbind("drag", onDrag);
|
||||||
|
eventHolder.unbind("dragend", onDragEnd);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: {
|
||||||
|
selection: {
|
||||||
|
mode: null, // one of null, "x", "y" or "xy"
|
||||||
|
visualization: "focus", // "focus" or "fill"
|
||||||
|
color: "#888888",
|
||||||
|
shape: "round", // one of "round", "miter", or "bevel"
|
||||||
|
minSize: 5 // minimum number of pixels
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'selection',
|
||||||
|
version: '1.1'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
/* Flot plugin for stacking data sets rather than overlaying them.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The plugin assumes the data is sorted on x (or y if stacking horizontally).
|
||||||
|
For line charts, it is assumed that if a line has an undefined gap (from a
|
||||||
|
null point), then the line above it should have the same gap - insert zeros
|
||||||
|
instead of "null" if you want another behaviour. This also holds for the start
|
||||||
|
and end of the chart. Note that stacking a mix of positive and negative values
|
||||||
|
in most instances doesn't make sense (so it looks weird).
|
||||||
|
|
||||||
|
Two or more series are stacked when their "stack" attribute is set to the same
|
||||||
|
key (which can be any number or string or just "true"). To specify the default
|
||||||
|
stack, you can set the stack option like this:
|
||||||
|
|
||||||
|
series: {
|
||||||
|
stack: null/false, true, or a key (number/string)
|
||||||
|
}
|
||||||
|
|
||||||
|
You can also specify it for a single series, like this:
|
||||||
|
|
||||||
|
$.plot( $("#placeholder"), [{
|
||||||
|
data: [ ... ],
|
||||||
|
stack: true
|
||||||
|
}])
|
||||||
|
|
||||||
|
The stacking order is determined by the order of the data series in the array
|
||||||
|
(later series end up on top of the previous).
|
||||||
|
|
||||||
|
Internally, the plugin modifies the datapoints in each series, adding an
|
||||||
|
offset to the y value. For line series, extra data points are inserted through
|
||||||
|
interpolation. If there's a second y value, it's also adjusted (e.g for bar
|
||||||
|
charts or filled areas).
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: { stack: null } // or number/string
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function findMatchingSeries(s, allseries) {
|
||||||
|
var res = null;
|
||||||
|
for (var i = 0; i < allseries.length; ++i) {
|
||||||
|
if (s === allseries[i]) break;
|
||||||
|
|
||||||
|
if (allseries[i].stack === s.stack) {
|
||||||
|
res = allseries[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addBottomPoints (s, datapoints) {
|
||||||
|
var formattedPoints = [];
|
||||||
|
for (var i = 0; i < datapoints.points.length; i += 2) {
|
||||||
|
formattedPoints.push(datapoints.points[i]);
|
||||||
|
formattedPoints.push(datapoints.points[i + 1]);
|
||||||
|
formattedPoints.push(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.format.push({
|
||||||
|
x: false,
|
||||||
|
y: true,
|
||||||
|
number: true,
|
||||||
|
required: false,
|
||||||
|
computeRange: s.yaxis.options.autoScale !== 'none',
|
||||||
|
defaultValue: 0
|
||||||
|
});
|
||||||
|
datapoints.points = formattedPoints;
|
||||||
|
datapoints.pointsize = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
function stackData(plot, s, datapoints) {
|
||||||
|
if (s.stack == null || s.stack === false) return;
|
||||||
|
|
||||||
|
var needsBottom = s.bars.show || (s.lines.show && s.lines.fill);
|
||||||
|
var hasBottom = datapoints.pointsize > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y);
|
||||||
|
// Series data is missing bottom points - need to format
|
||||||
|
if (needsBottom && !hasBottom) {
|
||||||
|
addBottomPoints(s, datapoints);
|
||||||
|
}
|
||||||
|
|
||||||
|
var other = findMatchingSeries(s, plot.getData());
|
||||||
|
if (!other) return;
|
||||||
|
|
||||||
|
var ps = datapoints.pointsize,
|
||||||
|
points = datapoints.points,
|
||||||
|
otherps = other.datapoints.pointsize,
|
||||||
|
otherpoints = other.datapoints.points,
|
||||||
|
newpoints = [],
|
||||||
|
px, py, intery, qx, qy, bottom,
|
||||||
|
withlines = s.lines.show,
|
||||||
|
horizontal = s.bars.horizontal,
|
||||||
|
withsteps = withlines && s.lines.steps,
|
||||||
|
fromgap = true,
|
||||||
|
keyOffset = horizontal ? 1 : 0,
|
||||||
|
accumulateOffset = horizontal ? 0 : 1,
|
||||||
|
i = 0, j = 0, l, m;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (i >= points.length) break;
|
||||||
|
|
||||||
|
l = newpoints.length;
|
||||||
|
|
||||||
|
if (points[i] == null) {
|
||||||
|
// copy gaps
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
} else if (j >= otherpoints.length) {
|
||||||
|
// for lines, we can't use the rest of the points
|
||||||
|
if (!withlines) {
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
} else if (otherpoints[j] == null) {
|
||||||
|
// oops, got a gap
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
fromgap = true;
|
||||||
|
j += otherps;
|
||||||
|
} else {
|
||||||
|
// cases where we actually got two points
|
||||||
|
px = points[i + keyOffset];
|
||||||
|
py = points[i + accumulateOffset];
|
||||||
|
qx = otherpoints[j + keyOffset];
|
||||||
|
qy = otherpoints[j + accumulateOffset];
|
||||||
|
bottom = 0;
|
||||||
|
|
||||||
|
if (px === qx) {
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
newpoints[l + accumulateOffset] += qy;
|
||||||
|
bottom = qy;
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
j += otherps;
|
||||||
|
} else if (px > qx) {
|
||||||
|
// we got past point below, might need to
|
||||||
|
// insert interpolated extra point
|
||||||
|
if (withlines && i > 0 && points[i - ps] != null) {
|
||||||
|
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
|
||||||
|
newpoints.push(qx);
|
||||||
|
newpoints.push(intery + qy);
|
||||||
|
for (m = 2; m < ps; ++m) {
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom = qy;
|
||||||
|
}
|
||||||
|
|
||||||
|
j += otherps;
|
||||||
|
} else { // px < qx
|
||||||
|
if (fromgap && withlines) {
|
||||||
|
// if we come from a gap, we just skip this point
|
||||||
|
i += ps;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints.push(points[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we might be able to interpolate a point below,
|
||||||
|
// this can give us a better y
|
||||||
|
if (withlines && j > 0 && otherpoints[j - otherps] != null) {
|
||||||
|
bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx);
|
||||||
|
}
|
||||||
|
|
||||||
|
newpoints[l + accumulateOffset] += bottom;
|
||||||
|
|
||||||
|
i += ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
fromgap = false;
|
||||||
|
|
||||||
|
if (l !== newpoints.length && needsBottom) {
|
||||||
|
newpoints[l + 2] += bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// maintain the line steps invariant
|
||||||
|
if (withsteps && l !== newpoints.length && l > 0 &&
|
||||||
|
newpoints[l] !== null &&
|
||||||
|
newpoints[l] !== newpoints[l - ps] &&
|
||||||
|
newpoints[l + 1] !== newpoints[l - ps + 1]) {
|
||||||
|
for (m = 0; m < ps; ++m) {
|
||||||
|
newpoints[l + ps + m] = newpoints[l + m];
|
||||||
|
}
|
||||||
|
|
||||||
|
newpoints[l + 1] = newpoints[l - ps + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.points = newpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(stackData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'stack',
|
||||||
|
version: '1.2'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/* Flot plugin that adds some extra symbols for plotting points.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The symbols are accessed as strings through the standard symbol options:
|
||||||
|
|
||||||
|
series: {
|
||||||
|
points: {
|
||||||
|
symbol: "square" // or "diamond", "triangle", "cross", "plus", "ellipse", "rectangle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
// we normalize the area of each symbol so it is approximately the
|
||||||
|
// same as a circle of the given radius
|
||||||
|
|
||||||
|
var square = function (ctx, x, y, radius, shadow) {
|
||||||
|
// pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
|
||||||
|
var size = radius * Math.sqrt(Math.PI) / 2;
|
||||||
|
ctx.rect(x - size, y - size, size + size, size + size);
|
||||||
|
},
|
||||||
|
rectangle = function (ctx, x, y, radius, shadow) {
|
||||||
|
// pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
|
||||||
|
var size = radius * Math.sqrt(Math.PI) / 2;
|
||||||
|
ctx.rect(x - size, y - size, size + size, size + size);
|
||||||
|
},
|
||||||
|
diamond = function (ctx, x, y, radius, shadow) {
|
||||||
|
// pi * r^2 = 2s^2 => s = r * sqrt(pi/2)
|
||||||
|
var size = radius * Math.sqrt(Math.PI / 2);
|
||||||
|
ctx.moveTo(x - size, y);
|
||||||
|
ctx.lineTo(x, y - size);
|
||||||
|
ctx.lineTo(x + size, y);
|
||||||
|
ctx.lineTo(x, y + size);
|
||||||
|
ctx.lineTo(x - size, y);
|
||||||
|
ctx.lineTo(x, y - size);
|
||||||
|
},
|
||||||
|
triangle = function (ctx, x, y, radius, shadow) {
|
||||||
|
// pi * r^2 = 1/2 * s^2 * sin (pi / 3) => s = r * sqrt(2 * pi / sin(pi / 3))
|
||||||
|
var size = radius * Math.sqrt(2 * Math.PI / Math.sin(Math.PI / 3));
|
||||||
|
var height = size * Math.sin(Math.PI / 3);
|
||||||
|
ctx.moveTo(x - size / 2, y + height / 2);
|
||||||
|
ctx.lineTo(x + size / 2, y + height / 2);
|
||||||
|
if (!shadow) {
|
||||||
|
ctx.lineTo(x, y - height / 2);
|
||||||
|
ctx.lineTo(x - size / 2, y + height / 2);
|
||||||
|
ctx.lineTo(x + size / 2, y + height / 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cross = function (ctx, x, y, radius, shadow) {
|
||||||
|
// pi * r^2 = (2s)^2 => s = r * sqrt(pi)/2
|
||||||
|
var size = radius * Math.sqrt(Math.PI) / 2;
|
||||||
|
ctx.moveTo(x - size, y - size);
|
||||||
|
ctx.lineTo(x + size, y + size);
|
||||||
|
ctx.moveTo(x - size, y + size);
|
||||||
|
ctx.lineTo(x + size, y - size);
|
||||||
|
},
|
||||||
|
ellipse = function(ctx, x, y, radius, shadow, fill) {
|
||||||
|
if (!shadow) {
|
||||||
|
ctx.moveTo(x + radius, y);
|
||||||
|
ctx.arc(x, y, radius, 0, Math.PI * 2, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plus = function (ctx, x, y, radius, shadow) {
|
||||||
|
var size = radius * Math.sqrt(Math.PI / 2);
|
||||||
|
ctx.moveTo(x - size, y);
|
||||||
|
ctx.lineTo(x + size, y);
|
||||||
|
ctx.moveTo(x, y + size);
|
||||||
|
ctx.lineTo(x, y - size);
|
||||||
|
},
|
||||||
|
handlers = {
|
||||||
|
square: square,
|
||||||
|
rectangle: rectangle,
|
||||||
|
diamond: diamond,
|
||||||
|
triangle: triangle,
|
||||||
|
cross: cross,
|
||||||
|
ellipse: ellipse,
|
||||||
|
plus: plus
|
||||||
|
};
|
||||||
|
|
||||||
|
square.fill = true;
|
||||||
|
rectangle.fill = true;
|
||||||
|
diamond.fill = true;
|
||||||
|
triangle.fill = true;
|
||||||
|
ellipse.fill = true;
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.drawSymbol = handlers;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
name: 'symbols',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/* Flot plugin for thresholding data.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
The plugin supports these options:
|
||||||
|
|
||||||
|
series: {
|
||||||
|
threshold: {
|
||||||
|
below: number
|
||||||
|
color: colorspec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
It can also be applied to a single series, like this:
|
||||||
|
|
||||||
|
$.plot( $("#placeholder"), [{
|
||||||
|
data: [ ... ],
|
||||||
|
threshold: { ... }
|
||||||
|
}])
|
||||||
|
|
||||||
|
An array can be passed for multiple thresholding, like this:
|
||||||
|
|
||||||
|
threshold: [{
|
||||||
|
below: number1
|
||||||
|
color: color1
|
||||||
|
},{
|
||||||
|
below: number2
|
||||||
|
color: color2
|
||||||
|
}]
|
||||||
|
|
||||||
|
These multiple threshold objects can be passed in any order since they are
|
||||||
|
sorted by the processing function.
|
||||||
|
|
||||||
|
The data points below "below" are drawn with the specified color. This makes
|
||||||
|
it easy to mark points below 0, e.g. for budget data.
|
||||||
|
|
||||||
|
Internally, the plugin works by splitting the data into two series, above and
|
||||||
|
below the threshold. The extra series below the threshold will have its label
|
||||||
|
cleared and the special "originSeries" attribute set to the original series.
|
||||||
|
You may need to check for this in hover events.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function ($) {
|
||||||
|
var options = {
|
||||||
|
series: { threshold: null } // or { below: number, color: color spec}
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
function thresholdData(plot, s, datapoints, below, color) {
|
||||||
|
var ps = datapoints.pointsize, i, x, y, p, prevp,
|
||||||
|
thresholded = $.extend({}, s); // note: shallow copy
|
||||||
|
|
||||||
|
thresholded.datapoints = { points: [], pointsize: ps, format: datapoints.format };
|
||||||
|
thresholded.label = null;
|
||||||
|
thresholded.color = color;
|
||||||
|
thresholded.threshold = null;
|
||||||
|
thresholded.originSeries = s;
|
||||||
|
thresholded.data = [];
|
||||||
|
|
||||||
|
var origpoints = datapoints.points,
|
||||||
|
addCrossingPoints = s.lines.show;
|
||||||
|
|
||||||
|
var threspoints = [];
|
||||||
|
var newpoints = [];
|
||||||
|
var m;
|
||||||
|
|
||||||
|
for (i = 0; i < origpoints.length; i += ps) {
|
||||||
|
x = origpoints[i];
|
||||||
|
y = origpoints[i + 1];
|
||||||
|
|
||||||
|
prevp = p;
|
||||||
|
if (y < below) p = threspoints;
|
||||||
|
else p = newpoints;
|
||||||
|
|
||||||
|
if (addCrossingPoints && prevp !== p &&
|
||||||
|
x !== null && i > 0 &&
|
||||||
|
origpoints[i - ps] != null) {
|
||||||
|
var interx = x + (below - y) * (x - origpoints[i - ps]) / (y - origpoints[i - ps + 1]);
|
||||||
|
prevp.push(interx);
|
||||||
|
prevp.push(below);
|
||||||
|
for (m = 2; m < ps; ++m) {
|
||||||
|
prevp.push(origpoints[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.push(null); // start new segment
|
||||||
|
p.push(null);
|
||||||
|
for (m = 2; m < ps; ++m) {
|
||||||
|
p.push(origpoints[i + m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
p.push(interx);
|
||||||
|
p.push(below);
|
||||||
|
for (m = 2; m < ps; ++m) {
|
||||||
|
p.push(origpoints[i + m]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.push(x);
|
||||||
|
p.push(y);
|
||||||
|
for (m = 2; m < ps; ++m) {
|
||||||
|
p.push(origpoints[i + m]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
datapoints.points = newpoints;
|
||||||
|
thresholded.datapoints.points = threspoints;
|
||||||
|
|
||||||
|
if (thresholded.datapoints.points.length > 0) {
|
||||||
|
var origIndex = $.inArray(s, plot.getData());
|
||||||
|
// Insert newly-generated series right after original one (to prevent it from becoming top-most)
|
||||||
|
plot.getData().splice(origIndex + 1, 0, thresholded);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: there are probably some edge cases left in bars
|
||||||
|
}
|
||||||
|
|
||||||
|
function processThresholds(plot, s, datapoints) {
|
||||||
|
if (!s.threshold) return;
|
||||||
|
if (s.threshold instanceof Array) {
|
||||||
|
s.threshold.sort(function(a, b) {
|
||||||
|
return a.below - b.below;
|
||||||
|
});
|
||||||
|
|
||||||
|
$(s.threshold).each(function(i, th) {
|
||||||
|
thresholdData(plot, s, datapoints, th.below, th.color);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
thresholdData(plot, s, datapoints, s.threshold.below, s.threshold.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plot.hooks.processDatapoints.push(processThresholds);
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'threshold',
|
||||||
|
version: '1.2'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,585 @@
|
|||||||
|
/* Pretty handling of time axes.
|
||||||
|
|
||||||
|
Copyright (c) 2007-2014 IOLA and Ole Laursen.
|
||||||
|
Licensed under the MIT license.
|
||||||
|
|
||||||
|
Set axis.mode to "time" to enable. See the section "Time series data" in
|
||||||
|
API.txt for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
xaxis: {
|
||||||
|
timezone: null, // "browser" for local to the client or timezone for timezone-js
|
||||||
|
timeformat: null, // format string to use
|
||||||
|
twelveHourClock: false, // 12 or 24 time in time mode
|
||||||
|
monthNames: null, // list of names of months
|
||||||
|
timeBase: 'seconds' // are the values in given in mircoseconds, milliseconds or seconds
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
timeBase: 'seconds'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var floorInBase = $.plot.saturated.floorInBase;
|
||||||
|
|
||||||
|
// Method to provide microsecond support to Date like classes.
|
||||||
|
var CreateMicroSecondDate = function(dateType, microEpoch) {
|
||||||
|
var newDate = new dateType(microEpoch);
|
||||||
|
|
||||||
|
var oldSetTime = newDate.setTime.bind(newDate);
|
||||||
|
newDate.update = function(microEpoch) {
|
||||||
|
oldSetTime(microEpoch);
|
||||||
|
|
||||||
|
// Round epoch to 3 decimal accuracy
|
||||||
|
microEpoch = Math.round(microEpoch*1000)/1000;
|
||||||
|
|
||||||
|
// Microseconds are stored as integers
|
||||||
|
this.microseconds = 1000 * (microEpoch - Math.floor(microEpoch));
|
||||||
|
};
|
||||||
|
|
||||||
|
var oldGetTime = newDate.getTime.bind(newDate);
|
||||||
|
newDate.getTime = function () {
|
||||||
|
var microEpoch = oldGetTime() + this.microseconds / 1000;
|
||||||
|
return microEpoch;
|
||||||
|
};
|
||||||
|
|
||||||
|
newDate.setTime = function (microEpoch) {
|
||||||
|
this.update(microEpoch);
|
||||||
|
};
|
||||||
|
|
||||||
|
newDate.getMicroseconds = function() {
|
||||||
|
return this.microseconds;
|
||||||
|
};
|
||||||
|
|
||||||
|
newDate.setMicroseconds = function(microseconds) {
|
||||||
|
var epochWithoutMicroseconds = oldGetTime();
|
||||||
|
var newEpoch = epochWithoutMicroseconds + microseconds/1000;
|
||||||
|
this.update(newEpoch);
|
||||||
|
};
|
||||||
|
|
||||||
|
newDate.setUTCMicroseconds = function(microseconds) { this.setMicroseconds(microseconds); }
|
||||||
|
|
||||||
|
newDate.getUTCMicroseconds = function() { return this.getMicroseconds(); }
|
||||||
|
|
||||||
|
newDate.microseconds = null;
|
||||||
|
newDate.microEpoch = null;
|
||||||
|
newDate.update(microEpoch);
|
||||||
|
return newDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a string with the date d formatted according to fmt.
|
||||||
|
// A subset of the Open Group's strftime format is supported.
|
||||||
|
|
||||||
|
function formatDate(d, fmt, monthNames, dayNames) {
|
||||||
|
if (typeof d.strftime === "function") {
|
||||||
|
return d.strftime(fmt);
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftPad = function(n, pad) {
|
||||||
|
n = "" + n;
|
||||||
|
pad = "" + (pad == null ? "0" : pad);
|
||||||
|
return n.length == 1 ? pad + n : n;
|
||||||
|
};
|
||||||
|
|
||||||
|
var formatSubSeconds = function(milliseconds, microseconds, numberDecimalPlaces) {
|
||||||
|
var totalMicroseconds = milliseconds * 1000 + microseconds;
|
||||||
|
var formattedString;
|
||||||
|
if (numberDecimalPlaces < 6 && numberDecimalPlaces > 0) {
|
||||||
|
var magnitude = parseFloat('1e' + (numberDecimalPlaces - 6));
|
||||||
|
totalMicroseconds = Math.round(Math.round(totalMicroseconds * magnitude) / magnitude);
|
||||||
|
formattedString = ('00000' + totalMicroseconds).slice(-6,-(6 - numberDecimalPlaces));
|
||||||
|
} else {
|
||||||
|
totalMicroseconds = Math.round(totalMicroseconds)
|
||||||
|
formattedString = ('00000' + totalMicroseconds).slice(-6);
|
||||||
|
}
|
||||||
|
return formattedString;
|
||||||
|
};
|
||||||
|
|
||||||
|
var r = [];
|
||||||
|
var escape = false;
|
||||||
|
var hours = d.getHours();
|
||||||
|
var isAM = hours < 12;
|
||||||
|
|
||||||
|
if (!monthNames) {
|
||||||
|
monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dayNames) {
|
||||||
|
dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var hours12;
|
||||||
|
if (hours > 12) {
|
||||||
|
hours12 = hours - 12;
|
||||||
|
} else if (hours == 0) {
|
||||||
|
hours12 = 12;
|
||||||
|
} else {
|
||||||
|
hours12 = hours;
|
||||||
|
}
|
||||||
|
|
||||||
|
var decimals = -1;
|
||||||
|
for (var i = 0; i < fmt.length; ++i) {
|
||||||
|
var c = fmt.charAt(i);
|
||||||
|
|
||||||
|
if (!isNaN(Number(c)) && Number(c) > 0) {
|
||||||
|
decimals = Number(c);
|
||||||
|
} else if (escape) {
|
||||||
|
switch (c) {
|
||||||
|
case 'a': c = "" + dayNames[d.getDay()]; break;
|
||||||
|
case 'b': c = "" + monthNames[d.getMonth()]; break;
|
||||||
|
case 'd': c = leftPad(d.getDate()); break;
|
||||||
|
case 'e': c = leftPad(d.getDate(), " "); break;
|
||||||
|
case 'h': // For back-compat with 0.7; remove in 1.0
|
||||||
|
case 'H': c = leftPad(hours); break;
|
||||||
|
case 'I': c = leftPad(hours12); break;
|
||||||
|
case 'l': c = leftPad(hours12, " "); break;
|
||||||
|
case 'm': c = leftPad(d.getMonth() + 1); break;
|
||||||
|
case 'M': c = leftPad(d.getMinutes()); break;
|
||||||
|
// quarters not in Open Group's strftime specification
|
||||||
|
case 'q':
|
||||||
|
c = "" + (Math.floor(d.getMonth() / 3) + 1); break;
|
||||||
|
case 'S': c = leftPad(d.getSeconds()); break;
|
||||||
|
case 's': c = "" + formatSubSeconds(d.getMilliseconds(), d.getMicroseconds(), decimals); break;
|
||||||
|
case 'y': c = leftPad(d.getFullYear() % 100); break;
|
||||||
|
case 'Y': c = "" + d.getFullYear(); break;
|
||||||
|
case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
|
||||||
|
case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
|
||||||
|
case 'w': c = "" + d.getDay(); break;
|
||||||
|
}
|
||||||
|
r.push(c);
|
||||||
|
escape = false;
|
||||||
|
} else {
|
||||||
|
if (c == "%") {
|
||||||
|
escape = true;
|
||||||
|
} else {
|
||||||
|
r.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// To have a consistent view of time-based data independent of which time
|
||||||
|
// zone the client happens to be in we need a date-like object independent
|
||||||
|
// of time zones. This is done through a wrapper that only calls the UTC
|
||||||
|
// versions of the accessor methods.
|
||||||
|
|
||||||
|
function makeUtcWrapper(d) {
|
||||||
|
function addProxyMethod(sourceObj, sourceMethod, targetObj, targetMethod) {
|
||||||
|
sourceObj[sourceMethod] = function() {
|
||||||
|
return targetObj[targetMethod].apply(targetObj, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var utc = {
|
||||||
|
date: d
|
||||||
|
};
|
||||||
|
|
||||||
|
// support strftime, if found
|
||||||
|
if (d.strftime !== undefined) {
|
||||||
|
addProxyMethod(utc, "strftime", d, "strftime");
|
||||||
|
}
|
||||||
|
|
||||||
|
addProxyMethod(utc, "getTime", d, "getTime");
|
||||||
|
addProxyMethod(utc, "setTime", d, "setTime");
|
||||||
|
|
||||||
|
var props = ["Date", "Day", "FullYear", "Hours", "Minutes", "Month", "Seconds", "Milliseconds", "Microseconds"];
|
||||||
|
|
||||||
|
for (var p = 0; p < props.length; p++) {
|
||||||
|
addProxyMethod(utc, "get" + props[p], d, "getUTC" + props[p]);
|
||||||
|
addProxyMethod(utc, "set" + props[p], d, "setUTC" + props[p]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return utc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// select time zone strategy. This returns a date-like object tied to the
|
||||||
|
// desired timezone
|
||||||
|
function dateGenerator(ts, opts) {
|
||||||
|
var maxDateValue = 8640000000000000;
|
||||||
|
|
||||||
|
if (opts && opts.timeBase === 'seconds') {
|
||||||
|
ts *= 1000;
|
||||||
|
} else if (opts.timeBase === 'microseconds') {
|
||||||
|
ts /= 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ts > maxDateValue) {
|
||||||
|
ts = maxDateValue;
|
||||||
|
} else if (ts < -maxDateValue) {
|
||||||
|
ts = -maxDateValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.timezone === "browser") {
|
||||||
|
return CreateMicroSecondDate(Date, ts);
|
||||||
|
} else if (!opts.timezone || opts.timezone === "utc") {
|
||||||
|
return makeUtcWrapper(CreateMicroSecondDate(Date, ts));
|
||||||
|
} else if (typeof timezoneJS !== "undefined" && typeof timezoneJS.Date !== "undefined") {
|
||||||
|
var d = CreateMicroSecondDate(timezoneJS.Date, ts);
|
||||||
|
// timezone-js is fickle, so be sure to set the time zone before
|
||||||
|
// setting the time.
|
||||||
|
d.setTimezone(opts.timezone);
|
||||||
|
d.setTime(ts);
|
||||||
|
return d;
|
||||||
|
} else {
|
||||||
|
return makeUtcWrapper(CreateMicroSecondDate(Date, ts));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// map of app. size of time units in seconds
|
||||||
|
var timeUnitSizeSeconds = {
|
||||||
|
"microsecond": 0.000001,
|
||||||
|
"millisecond": 0.001,
|
||||||
|
"second": 1,
|
||||||
|
"minute": 60,
|
||||||
|
"hour": 60 * 60,
|
||||||
|
"day": 24 * 60 * 60,
|
||||||
|
"month": 30 * 24 * 60 * 60,
|
||||||
|
"quarter": 3 * 30 * 24 * 60 * 60,
|
||||||
|
"year": 365.2425 * 24 * 60 * 60
|
||||||
|
};
|
||||||
|
|
||||||
|
// map of app. size of time units in milliseconds
|
||||||
|
var timeUnitSizeMilliseconds = {
|
||||||
|
"microsecond": 0.001,
|
||||||
|
"millisecond": 1,
|
||||||
|
"second": 1000,
|
||||||
|
"minute": 60 * 1000,
|
||||||
|
"hour": 60 * 60 * 1000,
|
||||||
|
"day": 24 * 60 * 60 * 1000,
|
||||||
|
"month": 30 * 24 * 60 * 60 * 1000,
|
||||||
|
"quarter": 3 * 30 * 24 * 60 * 60 * 1000,
|
||||||
|
"year": 365.2425 * 24 * 60 * 60 * 1000
|
||||||
|
};
|
||||||
|
|
||||||
|
// map of app. size of time units in microseconds
|
||||||
|
var timeUnitSizeMicroseconds = {
|
||||||
|
"microsecond": 1,
|
||||||
|
"millisecond": 1000,
|
||||||
|
"second": 1000000,
|
||||||
|
"minute": 60 * 1000000,
|
||||||
|
"hour": 60 * 60 * 1000000,
|
||||||
|
"day": 24 * 60 * 60 * 1000000,
|
||||||
|
"month": 30 * 24 * 60 * 60 * 1000000,
|
||||||
|
"quarter": 3 * 30 * 24 * 60 * 60 * 1000000,
|
||||||
|
"year": 365.2425 * 24 * 60 * 60 * 1000000
|
||||||
|
};
|
||||||
|
|
||||||
|
// the allowed tick sizes, after 1 year we use
|
||||||
|
// an integer algorithm
|
||||||
|
|
||||||
|
var baseSpec = [
|
||||||
|
[1, "microsecond"], [2, "microsecond"], [5, "microsecond"], [10, "microsecond"],
|
||||||
|
[25, "microsecond"], [50, "microsecond"], [100, "microsecond"], [250, "microsecond"], [500, "microsecond"],
|
||||||
|
[1, "millisecond"], [2, "millisecond"], [5, "millisecond"], [10, "millisecond"],
|
||||||
|
[25, "millisecond"], [50, "millisecond"], [100, "millisecond"], [250, "millisecond"], [500, "millisecond"],
|
||||||
|
[1, "second"], [2, "second"], [5, "second"], [10, "second"],
|
||||||
|
[30, "second"],
|
||||||
|
[1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
|
||||||
|
[30, "minute"],
|
||||||
|
[1, "hour"], [2, "hour"], [4, "hour"],
|
||||||
|
[8, "hour"], [12, "hour"],
|
||||||
|
[1, "day"], [2, "day"], [3, "day"],
|
||||||
|
[0.25, "month"], [0.5, "month"], [1, "month"],
|
||||||
|
[2, "month"]
|
||||||
|
];
|
||||||
|
|
||||||
|
// we don't know which variant(s) we'll need yet, but generating both is
|
||||||
|
// cheap
|
||||||
|
|
||||||
|
var specMonths = baseSpec.concat([[3, "month"], [6, "month"],
|
||||||
|
[1, "year"]]);
|
||||||
|
var specQuarters = baseSpec.concat([[1, "quarter"], [2, "quarter"],
|
||||||
|
[1, "year"]]);
|
||||||
|
|
||||||
|
|
||||||
|
function dateTickGenerator(axis) {
|
||||||
|
var opts = axis.options,
|
||||||
|
ticks = [],
|
||||||
|
d = dateGenerator(axis.min, opts),
|
||||||
|
minSize = 0;
|
||||||
|
|
||||||
|
// make quarter use a possibility if quarters are
|
||||||
|
// mentioned in either of these options
|
||||||
|
var spec = (opts.tickSize && opts.tickSize[1] ===
|
||||||
|
"quarter") ||
|
||||||
|
(opts.minTickSize && opts.minTickSize[1] ===
|
||||||
|
"quarter") ? specQuarters : specMonths;
|
||||||
|
|
||||||
|
var timeUnitSize;
|
||||||
|
if (opts.timeBase === 'seconds') {
|
||||||
|
timeUnitSize = timeUnitSizeSeconds;
|
||||||
|
} else if (opts.timeBase === 'microseconds') {
|
||||||
|
timeUnitSize = timeUnitSizeMicroseconds;
|
||||||
|
} else {
|
||||||
|
timeUnitSize = timeUnitSizeMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.minTickSize !== null && opts.minTickSize !== undefined) {
|
||||||
|
if (typeof opts.tickSize === "number") {
|
||||||
|
minSize = opts.tickSize;
|
||||||
|
} else {
|
||||||
|
minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < spec.length - 1; ++i) {
|
||||||
|
if (axis.delta < (spec[i][0] * timeUnitSize[spec[i][1]] +
|
||||||
|
spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2 &&
|
||||||
|
spec[i][0] * timeUnitSize[spec[i][1]] >= minSize) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = spec[i][0];
|
||||||
|
var unit = spec[i][1];
|
||||||
|
// special-case the possibility of several years
|
||||||
|
if (unit === "year") {
|
||||||
|
// if given a minTickSize in years, just use it,
|
||||||
|
// ensuring that it's an integer
|
||||||
|
|
||||||
|
if (opts.minTickSize !== null && opts.minTickSize !== undefined && opts.minTickSize[1] === "year") {
|
||||||
|
size = Math.floor(opts.minTickSize[0]);
|
||||||
|
} else {
|
||||||
|
var magn = parseFloat('1e' + Math.floor(Math.log(axis.delta / timeUnitSize.year) / Math.LN10));
|
||||||
|
var norm = (axis.delta / timeUnitSize.year) / magn;
|
||||||
|
|
||||||
|
if (norm < 1.5) {
|
||||||
|
size = 1;
|
||||||
|
} else if (norm < 3) {
|
||||||
|
size = 2;
|
||||||
|
} else if (norm < 7.5) {
|
||||||
|
size = 5;
|
||||||
|
} else {
|
||||||
|
size = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
size *= magn;
|
||||||
|
}
|
||||||
|
|
||||||
|
// minimum size for years is 1
|
||||||
|
|
||||||
|
if (size < 1) {
|
||||||
|
size = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
axis.tickSize = opts.tickSize || [size, unit];
|
||||||
|
var tickSize = axis.tickSize[0];
|
||||||
|
unit = axis.tickSize[1];
|
||||||
|
|
||||||
|
var step = tickSize * timeUnitSize[unit];
|
||||||
|
|
||||||
|
if (unit === "microsecond") {
|
||||||
|
d.setMicroseconds(floorInBase(d.getMicroseconds(), tickSize));
|
||||||
|
} else if (unit === "millisecond") {
|
||||||
|
d.setMilliseconds(floorInBase(d.getMilliseconds(), tickSize));
|
||||||
|
} else if (unit === "second") {
|
||||||
|
d.setSeconds(floorInBase(d.getSeconds(), tickSize));
|
||||||
|
} else if (unit === "minute") {
|
||||||
|
d.setMinutes(floorInBase(d.getMinutes(), tickSize));
|
||||||
|
} else if (unit === "hour") {
|
||||||
|
d.setHours(floorInBase(d.getHours(), tickSize));
|
||||||
|
} else if (unit === "month") {
|
||||||
|
d.setMonth(floorInBase(d.getMonth(), tickSize));
|
||||||
|
} else if (unit === "quarter") {
|
||||||
|
d.setMonth(3 * floorInBase(d.getMonth() / 3,
|
||||||
|
tickSize));
|
||||||
|
} else if (unit === "year") {
|
||||||
|
d.setFullYear(floorInBase(d.getFullYear(), tickSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset smaller components
|
||||||
|
|
||||||
|
if (step >= timeUnitSize.millisecond) {
|
||||||
|
if (step >= timeUnitSize.second) {
|
||||||
|
d.setMicroseconds(0);
|
||||||
|
} else {
|
||||||
|
d.setMicroseconds(d.getMilliseconds()*1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.minute) {
|
||||||
|
d.setSeconds(0);
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.hour) {
|
||||||
|
d.setMinutes(0);
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.day) {
|
||||||
|
d.setHours(0);
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.day * 4) {
|
||||||
|
d.setDate(1);
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.month * 2) {
|
||||||
|
d.setMonth(floorInBase(d.getMonth(), 3));
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.quarter * 2) {
|
||||||
|
d.setMonth(floorInBase(d.getMonth(), 6));
|
||||||
|
}
|
||||||
|
if (step >= timeUnitSize.year) {
|
||||||
|
d.setMonth(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var carry = 0;
|
||||||
|
var v = Number.NaN;
|
||||||
|
var v1000;
|
||||||
|
var prev;
|
||||||
|
do {
|
||||||
|
prev = v;
|
||||||
|
v1000 = d.getTime();
|
||||||
|
if (opts && opts.timeBase === 'seconds') {
|
||||||
|
v = v1000 / 1000;
|
||||||
|
} else if (opts && opts.timeBase === 'microseconds') {
|
||||||
|
v = v1000 * 1000;
|
||||||
|
} else {
|
||||||
|
v = v1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
ticks.push(v);
|
||||||
|
|
||||||
|
if (unit === "month" || unit === "quarter") {
|
||||||
|
if (tickSize < 1) {
|
||||||
|
// a bit complicated - we'll divide the
|
||||||
|
// month/quarter up but we need to take
|
||||||
|
// care of fractions so we don't end up in
|
||||||
|
// the middle of a day
|
||||||
|
d.setDate(1);
|
||||||
|
var start = d.getTime();
|
||||||
|
d.setMonth(d.getMonth() +
|
||||||
|
(unit === "quarter" ? 3 : 1));
|
||||||
|
var end = d.getTime();
|
||||||
|
d.setTime((v + carry * timeUnitSize.hour + (end - start) * tickSize));
|
||||||
|
carry = d.getHours();
|
||||||
|
d.setHours(0);
|
||||||
|
} else {
|
||||||
|
d.setMonth(d.getMonth() +
|
||||||
|
tickSize * (unit === "quarter" ? 3 : 1));
|
||||||
|
}
|
||||||
|
} else if (unit === "year") {
|
||||||
|
d.setFullYear(d.getFullYear() + tickSize);
|
||||||
|
} else {
|
||||||
|
if (opts.timeBase === 'seconds') {
|
||||||
|
d.setTime((v + step) * 1000);
|
||||||
|
} else if (opts.timeBase === 'microseconds') {
|
||||||
|
d.setTime((v + step) / 1000);
|
||||||
|
} else {
|
||||||
|
d.setTime(v + step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (v < axis.max && v !== prev);
|
||||||
|
|
||||||
|
return ticks;
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processOptions.push(function (plot) {
|
||||||
|
$.each(plot.getAxes(), function(axisName, axis) {
|
||||||
|
var opts = axis.options;
|
||||||
|
if (opts.mode === "time") {
|
||||||
|
axis.tickGenerator = dateTickGenerator;
|
||||||
|
|
||||||
|
axis.tickFormatter = function (v, axis) {
|
||||||
|
var d = dateGenerator(v, axis.options);
|
||||||
|
|
||||||
|
// first check global format
|
||||||
|
if (opts.timeformat != null) {
|
||||||
|
return formatDate(d, opts.timeformat, opts.monthNames, opts.dayNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// possibly use quarters if quarters are mentioned in
|
||||||
|
// any of these places
|
||||||
|
var useQuarters = (axis.options.tickSize &&
|
||||||
|
axis.options.tickSize[1] == "quarter") ||
|
||||||
|
(axis.options.minTickSize &&
|
||||||
|
axis.options.minTickSize[1] == "quarter");
|
||||||
|
|
||||||
|
var timeUnitSize;
|
||||||
|
if (opts.timeBase === 'seconds') {
|
||||||
|
timeUnitSize = timeUnitSizeSeconds;
|
||||||
|
} else if (opts.timeBase === 'microseconds') {
|
||||||
|
timeUnitSize = timeUnitSizeMicroseconds;
|
||||||
|
} else {
|
||||||
|
timeUnitSize = timeUnitSizeMilliseconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
|
||||||
|
var span = axis.max - axis.min;
|
||||||
|
var suffix = (opts.twelveHourClock) ? " %p" : "";
|
||||||
|
var hourCode = (opts.twelveHourClock) ? "%I" : "%H";
|
||||||
|
var factor;
|
||||||
|
var fmt;
|
||||||
|
|
||||||
|
if (opts.timeBase === 'seconds') {
|
||||||
|
factor = 1;
|
||||||
|
} else if (opts.timeBase === 'microseconds') {
|
||||||
|
factor = 1000000
|
||||||
|
} else {
|
||||||
|
factor = 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t < timeUnitSize.second) {
|
||||||
|
var decimals = -Math.floor(Math.log10(t/factor))
|
||||||
|
|
||||||
|
// the two-and-halves require an additional decimal
|
||||||
|
if (String(t).indexOf('25') > -1) {
|
||||||
|
decimals++;
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt = "%S.%" + decimals + "s";
|
||||||
|
} else
|
||||||
|
if (t < timeUnitSize.minute) {
|
||||||
|
fmt = hourCode + ":%M:%S" + suffix;
|
||||||
|
} else if (t < timeUnitSize.day) {
|
||||||
|
if (span < 2 * timeUnitSize.day) {
|
||||||
|
fmt = hourCode + ":%M" + suffix;
|
||||||
|
} else {
|
||||||
|
fmt = "%b %d " + hourCode + ":%M" + suffix;
|
||||||
|
}
|
||||||
|
} else if (t < timeUnitSize.month) {
|
||||||
|
fmt = "%b %d";
|
||||||
|
} else if ((useQuarters && t < timeUnitSize.quarter) ||
|
||||||
|
(!useQuarters && t < timeUnitSize.year)) {
|
||||||
|
if (span < timeUnitSize.year) {
|
||||||
|
fmt = "%b";
|
||||||
|
} else {
|
||||||
|
fmt = "%b %Y";
|
||||||
|
}
|
||||||
|
} else if (useQuarters && t < timeUnitSize.year) {
|
||||||
|
if (span < timeUnitSize.year) {
|
||||||
|
fmt = "Q%q";
|
||||||
|
} else {
|
||||||
|
fmt = "Q%q %Y";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt = "%Y";
|
||||||
|
}
|
||||||
|
|
||||||
|
var rt = formatDate(d, fmt, opts.monthNames, opts.dayNames);
|
||||||
|
|
||||||
|
return rt;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'time',
|
||||||
|
version: '1.0'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Time-axis support used to be in Flot core, which exposed the
|
||||||
|
// formatDate function on the plot object. Various plugins depend
|
||||||
|
// on the function, so we need to re-expose it here.
|
||||||
|
|
||||||
|
$.plot.formatDate = formatDate;
|
||||||
|
$.plot.dateGenerator = dateGenerator;
|
||||||
|
$.plot.dateTickGenerator = dateTickGenerator;
|
||||||
|
$.plot.makeUtcWrapper = makeUtcWrapper;
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
|
||||||
|
/* global jQuery */
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
propagateSupportedGesture: false
|
||||||
|
};
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processOptions.push(initTouchNavigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTouchNavigation(plot, options) {
|
||||||
|
var gestureState = {
|
||||||
|
twoTouches: false,
|
||||||
|
currentTapStart: { x: 0, y: 0 },
|
||||||
|
currentTapEnd: { x: 0, y: 0 },
|
||||||
|
prevTap: { x: 0, y: 0 },
|
||||||
|
currentTap: { x: 0, y: 0 },
|
||||||
|
interceptedLongTap: false,
|
||||||
|
isUnsupportedGesture: false,
|
||||||
|
prevTapTime: null,
|
||||||
|
tapStartTime: null,
|
||||||
|
longTapTriggerId: null
|
||||||
|
},
|
||||||
|
maxDistanceBetweenTaps = 20,
|
||||||
|
maxIntervalBetweenTaps = 500,
|
||||||
|
maxLongTapDistance = 20,
|
||||||
|
minLongTapDuration = 1500,
|
||||||
|
pressedTapDuration = 125,
|
||||||
|
mainEventHolder;
|
||||||
|
|
||||||
|
function interpretGestures(e) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
if (!o.pan.active && !o.zoom.active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOnMultipleTouches(e);
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('touchevent', { detail: e }));
|
||||||
|
|
||||||
|
if (isPinchEvent(e)) {
|
||||||
|
executeAction(e, 'pinch');
|
||||||
|
} else {
|
||||||
|
executeAction(e, 'pan');
|
||||||
|
if (!wasPinchEvent(e)) {
|
||||||
|
if (isDoubleTap(e)) {
|
||||||
|
executeAction(e, 'doubleTap');
|
||||||
|
}
|
||||||
|
executeAction(e, 'tap');
|
||||||
|
executeAction(e, 'longTap');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeAction(e, gesture) {
|
||||||
|
switch (gesture) {
|
||||||
|
case 'pan':
|
||||||
|
pan[e.type](e);
|
||||||
|
break;
|
||||||
|
case 'pinch':
|
||||||
|
pinch[e.type](e);
|
||||||
|
break;
|
||||||
|
case 'doubleTap':
|
||||||
|
doubleTap.onDoubleTap(e);
|
||||||
|
break;
|
||||||
|
case 'longTap':
|
||||||
|
longTap[e.type](e);
|
||||||
|
break;
|
||||||
|
case 'tap':
|
||||||
|
tap[e.type](e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bindEvents(plot, eventHolder) {
|
||||||
|
mainEventHolder = eventHolder[0];
|
||||||
|
eventHolder[0].addEventListener('touchstart', interpretGestures, false);
|
||||||
|
eventHolder[0].addEventListener('touchmove', interpretGestures, false);
|
||||||
|
eventHolder[0].addEventListener('touchend', interpretGestures, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(plot, eventHolder) {
|
||||||
|
eventHolder[0].removeEventListener('touchstart', interpretGestures);
|
||||||
|
eventHolder[0].removeEventListener('touchmove', interpretGestures);
|
||||||
|
eventHolder[0].removeEventListener('touchend', interpretGestures);
|
||||||
|
if (gestureState.longTapTriggerId) {
|
||||||
|
clearTimeout(gestureState.longTapTriggerId);
|
||||||
|
gestureState.longTapTriggerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pan = {
|
||||||
|
touchstart: function(e) {
|
||||||
|
updatePrevForDoubleTap();
|
||||||
|
updateCurrentForDoubleTap(e);
|
||||||
|
updateStateForLongTapStart(e);
|
||||||
|
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('panstart', { detail: e }));
|
||||||
|
},
|
||||||
|
|
||||||
|
touchmove: function(e) {
|
||||||
|
preventEventBehaviors(e);
|
||||||
|
|
||||||
|
updateCurrentForDoubleTap(e);
|
||||||
|
updateStateForLongTapEnd(e);
|
||||||
|
|
||||||
|
if (!gestureState.isUnsupportedGesture) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('pandrag', { detail: e }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
touchend: function(e) {
|
||||||
|
preventEventBehaviors(e);
|
||||||
|
|
||||||
|
if (wasPinchEvent(e)) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('pinchend', { detail: e }));
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('panstart', { detail: e }));
|
||||||
|
} else if (noTouchActive(e)) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('panend', { detail: e }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var pinch = {
|
||||||
|
touchstart: function(e) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('pinchstart', { detail: e }));
|
||||||
|
},
|
||||||
|
|
||||||
|
touchmove: function(e) {
|
||||||
|
preventEventBehaviors(e);
|
||||||
|
gestureState.twoTouches = isPinchEvent(e);
|
||||||
|
if (!gestureState.isUnsupportedGesture) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('pinchdrag', { detail: e }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
touchend: function(e) {
|
||||||
|
preventEventBehaviors(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var doubleTap = {
|
||||||
|
onDoubleTap: function(e) {
|
||||||
|
preventEventBehaviors(e);
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('doubletap', { detail: e }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var longTap = {
|
||||||
|
touchstart: function(e) {
|
||||||
|
longTap.waitForLongTap(e);
|
||||||
|
},
|
||||||
|
|
||||||
|
touchmove: function(e) {
|
||||||
|
},
|
||||||
|
|
||||||
|
touchend: function(e) {
|
||||||
|
if (gestureState.longTapTriggerId) {
|
||||||
|
clearTimeout(gestureState.longTapTriggerId);
|
||||||
|
gestureState.longTapTriggerId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isLongTap: function(e) {
|
||||||
|
var currentTime = new Date().getTime(),
|
||||||
|
tapDuration = currentTime - gestureState.tapStartTime;
|
||||||
|
if (tapDuration >= minLongTapDuration && !gestureState.interceptedLongTap) {
|
||||||
|
if (distance(gestureState.currentTapStart.x, gestureState.currentTapStart.y, gestureState.currentTapEnd.x, gestureState.currentTapEnd.y) < maxLongTapDistance) {
|
||||||
|
gestureState.interceptedLongTap = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
waitForLongTap: function(e) {
|
||||||
|
var longTapTrigger = function() {
|
||||||
|
if (longTap.isLongTap(e)) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('longtap', { detail: e }));
|
||||||
|
}
|
||||||
|
gestureState.longTapTriggerId = null;
|
||||||
|
};
|
||||||
|
if (!gestureState.longTapTriggerId) {
|
||||||
|
gestureState.longTapTriggerId = setTimeout(longTapTrigger, minLongTapDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var tap = {
|
||||||
|
touchstart: function(e) {
|
||||||
|
gestureState.tapStartTime = new Date().getTime();
|
||||||
|
},
|
||||||
|
|
||||||
|
touchmove: function(e) {
|
||||||
|
},
|
||||||
|
|
||||||
|
touchend: function(e) {
|
||||||
|
if (tap.isTap(e)) {
|
||||||
|
mainEventHolder.dispatchEvent(new CustomEvent('tap', { detail: e }));
|
||||||
|
preventEventBehaviors(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isTap: function(e) {
|
||||||
|
var currentTime = new Date().getTime(),
|
||||||
|
tapDuration = currentTime - gestureState.tapStartTime;
|
||||||
|
if (tapDuration <= pressedTapDuration) {
|
||||||
|
if (distance(gestureState.currentTapStart.x, gestureState.currentTapStart.y, gestureState.currentTapEnd.x, gestureState.currentTapEnd.y) < maxLongTapDistance) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.pan.enableTouch === true || options.zoom.enableTouch) {
|
||||||
|
plot.hooks.bindEvents.push(bindEvents);
|
||||||
|
plot.hooks.shutdown.push(shutdown);
|
||||||
|
};
|
||||||
|
|
||||||
|
function updatePrevForDoubleTap() {
|
||||||
|
gestureState.prevTap = {
|
||||||
|
x: gestureState.currentTap.x,
|
||||||
|
y: gestureState.currentTap.y
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateCurrentForDoubleTap(e) {
|
||||||
|
gestureState.currentTap = {
|
||||||
|
x: e.touches[0].pageX,
|
||||||
|
y: e.touches[0].pageY
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStateForLongTapStart(e) {
|
||||||
|
gestureState.tapStartTime = new Date().getTime();
|
||||||
|
gestureState.interceptedLongTap = false;
|
||||||
|
gestureState.currentTapStart = {
|
||||||
|
x: e.touches[0].pageX,
|
||||||
|
y: e.touches[0].pageY
|
||||||
|
};
|
||||||
|
gestureState.currentTapEnd = {
|
||||||
|
x: e.touches[0].pageX,
|
||||||
|
y: e.touches[0].pageY
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateStateForLongTapEnd(e) {
|
||||||
|
gestureState.currentTapEnd = {
|
||||||
|
x: e.touches[0].pageX,
|
||||||
|
y: e.touches[0].pageY
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function isDoubleTap(e) {
|
||||||
|
var currentTime = new Date().getTime(),
|
||||||
|
intervalBetweenTaps = currentTime - gestureState.prevTapTime;
|
||||||
|
|
||||||
|
if (intervalBetweenTaps >= 0 && intervalBetweenTaps < maxIntervalBetweenTaps) {
|
||||||
|
if (distance(gestureState.prevTap.x, gestureState.prevTap.y, gestureState.currentTap.x, gestureState.currentTap.y) < maxDistanceBetweenTaps) {
|
||||||
|
e.firstTouch = gestureState.prevTap;
|
||||||
|
e.secondTouch = gestureState.currentTap;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gestureState.prevTapTime = currentTime;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function preventEventBehaviors(e) {
|
||||||
|
if (!gestureState.isUnsupportedGesture) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!plot.getOptions().propagateSupportedGesture) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(x1, y1, x2, y2) {
|
||||||
|
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function noTouchActive(e) {
|
||||||
|
return (e.touches && e.touches.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasPinchEvent(e) {
|
||||||
|
return (gestureState.twoTouches && e.touches.length === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOnMultipleTouches(e) {
|
||||||
|
if (e.touches.length >= 3) {
|
||||||
|
gestureState.isUnsupportedGesture = true;
|
||||||
|
} else {
|
||||||
|
gestureState.isUnsupportedGesture = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPinchEvent(e) {
|
||||||
|
if (e.touches && e.touches.length >= 2) {
|
||||||
|
if (e.touches[0].target === plot.getEventHolder() &&
|
||||||
|
e.touches[1].target === plot.getEventHolder()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'navigateTouch',
|
||||||
|
version: '0.3'
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
/* global jQuery */
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
zoom: {
|
||||||
|
enableTouch: false
|
||||||
|
},
|
||||||
|
pan: {
|
||||||
|
enableTouch: false,
|
||||||
|
touchMode: 'manual'
|
||||||
|
},
|
||||||
|
recenter: {
|
||||||
|
enableTouch: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var ZOOM_DISTANCE_MARGIN = $.plot.uiConstants.ZOOM_DISTANCE_MARGIN;
|
||||||
|
|
||||||
|
function init(plot) {
|
||||||
|
plot.hooks.processOptions.push(initTouchNavigation);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTouchNavigation(plot, options) {
|
||||||
|
var gestureState = {
|
||||||
|
zoomEnable: false,
|
||||||
|
prevDistance: null,
|
||||||
|
prevTapTime: 0,
|
||||||
|
prevPanPosition: { x: 0, y: 0 },
|
||||||
|
prevTapPosition: { x: 0, y: 0 }
|
||||||
|
},
|
||||||
|
navigationState = {
|
||||||
|
prevTouchedAxis: 'none',
|
||||||
|
currentTouchedAxis: 'none',
|
||||||
|
touchedAxis: null,
|
||||||
|
navigationConstraint: 'unconstrained',
|
||||||
|
initialState: null,
|
||||||
|
},
|
||||||
|
useManualPan = options.pan.interactive && options.pan.touchMode === 'manual',
|
||||||
|
smartPanLock = options.pan.touchMode === 'smartLock',
|
||||||
|
useSmartPan = options.pan.interactive && (smartPanLock || options.pan.touchMode === 'smart'),
|
||||||
|
pan, pinch, doubleTap;
|
||||||
|
|
||||||
|
function bindEvents(plot, eventHolder) {
|
||||||
|
var o = plot.getOptions();
|
||||||
|
|
||||||
|
if (o.zoom.interactive && o.zoom.enableTouch) {
|
||||||
|
eventHolder[0].addEventListener('pinchstart', pinch.start, false);
|
||||||
|
eventHolder[0].addEventListener('pinchdrag', pinch.drag, false);
|
||||||
|
eventHolder[0].addEventListener('pinchend', pinch.end, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o.pan.interactive && o.pan.enableTouch) {
|
||||||
|
eventHolder[0].addEventListener('panstart', pan.start, false);
|
||||||
|
eventHolder[0].addEventListener('pandrag', pan.drag, false);
|
||||||
|
eventHolder[0].addEventListener('panend', pan.end, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((o.recenter.interactive && o.recenter.enableTouch)) {
|
||||||
|
eventHolder[0].addEventListener('doubletap', doubleTap.recenterPlot, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(plot, eventHolder) {
|
||||||
|
eventHolder[0].removeEventListener('panstart', pan.start);
|
||||||
|
eventHolder[0].removeEventListener('pandrag', pan.drag);
|
||||||
|
eventHolder[0].removeEventListener('panend', pan.end);
|
||||||
|
eventHolder[0].removeEventListener('pinchstart', pinch.start);
|
||||||
|
eventHolder[0].removeEventListener('pinchdrag', pinch.drag);
|
||||||
|
eventHolder[0].removeEventListener('pinchend', pinch.end);
|
||||||
|
eventHolder[0].removeEventListener('doubletap', doubleTap.recenterPlot);
|
||||||
|
}
|
||||||
|
|
||||||
|
pan = {
|
||||||
|
start: function(e) {
|
||||||
|
presetNavigationState(e, 'pan', gestureState);
|
||||||
|
updateData(e, 'pan', gestureState, navigationState);
|
||||||
|
|
||||||
|
if (useSmartPan) {
|
||||||
|
var point = getPoint(e, 'pan');
|
||||||
|
navigationState.initialState = plot.navigationState(point.x, point.y);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
drag: function(e) {
|
||||||
|
presetNavigationState(e, 'pan', gestureState);
|
||||||
|
|
||||||
|
if (useSmartPan) {
|
||||||
|
var point = getPoint(e, 'pan');
|
||||||
|
plot.smartPan({
|
||||||
|
x: navigationState.initialState.startPageX - point.x,
|
||||||
|
y: navigationState.initialState.startPageY - point.y
|
||||||
|
}, navigationState.initialState, navigationState.touchedAxis, false, smartPanLock);
|
||||||
|
} else if (useManualPan) {
|
||||||
|
plot.pan({
|
||||||
|
left: -delta(e, 'pan', gestureState).x,
|
||||||
|
top: -delta(e, 'pan', gestureState).y,
|
||||||
|
axes: navigationState.touchedAxis
|
||||||
|
});
|
||||||
|
updatePrevPanPosition(e, 'pan', gestureState, navigationState);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
end: function(e) {
|
||||||
|
presetNavigationState(e, 'pan', gestureState);
|
||||||
|
|
||||||
|
if (useSmartPan) {
|
||||||
|
plot.smartPan.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasPinchEvent(e, gestureState)) {
|
||||||
|
updateprevPanPosition(e, 'pan', gestureState, navigationState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var pinchDragTimeout;
|
||||||
|
pinch = {
|
||||||
|
start: function(e) {
|
||||||
|
if (pinchDragTimeout) {
|
||||||
|
clearTimeout(pinchDragTimeout);
|
||||||
|
pinchDragTimeout = null;
|
||||||
|
}
|
||||||
|
presetNavigationState(e, 'pinch', gestureState);
|
||||||
|
setPrevDistance(e, gestureState);
|
||||||
|
updateData(e, 'pinch', gestureState, navigationState);
|
||||||
|
},
|
||||||
|
|
||||||
|
drag: function(e) {
|
||||||
|
if (pinchDragTimeout) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pinchDragTimeout = setTimeout(function() {
|
||||||
|
presetNavigationState(e, 'pinch', gestureState);
|
||||||
|
plot.pan({
|
||||||
|
left: -delta(e, 'pinch', gestureState).x,
|
||||||
|
top: -delta(e, 'pinch', gestureState).y,
|
||||||
|
axes: navigationState.touchedAxis
|
||||||
|
});
|
||||||
|
updatePrevPanPosition(e, 'pinch', gestureState, navigationState);
|
||||||
|
|
||||||
|
var dist = pinchDistance(e);
|
||||||
|
|
||||||
|
if (gestureState.zoomEnable || Math.abs(dist - gestureState.prevDistance) > ZOOM_DISTANCE_MARGIN) {
|
||||||
|
zoomPlot(plot, e, gestureState, navigationState);
|
||||||
|
|
||||||
|
//activate zoom mode
|
||||||
|
gestureState.zoomEnable = true;
|
||||||
|
}
|
||||||
|
pinchDragTimeout = null;
|
||||||
|
}, 1000 / 60);
|
||||||
|
},
|
||||||
|
|
||||||
|
end: function(e) {
|
||||||
|
if (pinchDragTimeout) {
|
||||||
|
clearTimeout(pinchDragTimeout);
|
||||||
|
pinchDragTimeout = null;
|
||||||
|
}
|
||||||
|
presetNavigationState(e, 'pinch', gestureState);
|
||||||
|
gestureState.prevDistance = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
doubleTap = {
|
||||||
|
recenterPlot: function(e) {
|
||||||
|
if (e && e.detail && e.detail.type === 'touchstart') {
|
||||||
|
// only do not recenter for touch start;
|
||||||
|
recenterPlotOnDoubleTap(plot, e, gestureState, navigationState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.pan.enableTouch === true || options.zoom.enableTouch === true) {
|
||||||
|
plot.hooks.bindEvents.push(bindEvents);
|
||||||
|
plot.hooks.shutdown.push(shutdown);
|
||||||
|
}
|
||||||
|
|
||||||
|
function presetNavigationState(e, gesture, gestureState) {
|
||||||
|
navigationState.touchedAxis = getAxis(plot, e, gesture, navigationState);
|
||||||
|
if (noAxisTouched(navigationState)) {
|
||||||
|
navigationState.navigationConstraint = 'unconstrained';
|
||||||
|
} else {
|
||||||
|
navigationState.navigationConstraint = 'axisConstrained';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$.plot.plugins.push({
|
||||||
|
init: init,
|
||||||
|
options: options,
|
||||||
|
name: 'navigateTouch',
|
||||||
|
version: '0.3'
|
||||||
|
});
|
||||||
|
|
||||||
|
function recenterPlotOnDoubleTap(plot, e, gestureState, navigationState) {
|
||||||
|
checkAxesForDoubleTap(plot, e, navigationState);
|
||||||
|
if ((navigationState.currentTouchedAxis === 'x' && navigationState.prevTouchedAxis === 'x') ||
|
||||||
|
(navigationState.currentTouchedAxis === 'y' && navigationState.prevTouchedAxis === 'y') ||
|
||||||
|
(navigationState.currentTouchedAxis === 'none' && navigationState.prevTouchedAxis === 'none')) {
|
||||||
|
var event;
|
||||||
|
|
||||||
|
plot.recenter({ axes: navigationState.touchedAxis });
|
||||||
|
|
||||||
|
if (navigationState.touchedAxis) {
|
||||||
|
event = new $.Event('re-center', { detail: { axisTouched: navigationState.touchedAxis } });
|
||||||
|
} else {
|
||||||
|
event = new $.Event('re-center', { detail: e });
|
||||||
|
}
|
||||||
|
plot.getPlaceholder().trigger(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkAxesForDoubleTap(plot, e, navigationState) {
|
||||||
|
var axis = plot.getTouchedAxis(e.detail.firstTouch.x, e.detail.firstTouch.y);
|
||||||
|
if (axis[0] !== undefined) {
|
||||||
|
navigationState.prevTouchedAxis = axis[0].direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
axis = plot.getTouchedAxis(e.detail.secondTouch.x, e.detail.secondTouch.y);
|
||||||
|
if (axis[0] !== undefined) {
|
||||||
|
navigationState.touchedAxis = axis;
|
||||||
|
navigationState.currentTouchedAxis = axis[0].direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noAxisTouched(navigationState)) {
|
||||||
|
navigationState.touchedAxis = null;
|
||||||
|
navigationState.prevTouchedAxis = 'none';
|
||||||
|
navigationState.currentTouchedAxis = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomPlot(plot, e, gestureState, navigationState) {
|
||||||
|
var offset = plot.offset(),
|
||||||
|
center = {
|
||||||
|
left: 0,
|
||||||
|
top: 0
|
||||||
|
},
|
||||||
|
zoomAmount = pinchDistance(e) / gestureState.prevDistance,
|
||||||
|
dist = pinchDistance(e);
|
||||||
|
|
||||||
|
center.left = getPoint(e, 'pinch').x - offset.left;
|
||||||
|
center.top = getPoint(e, 'pinch').y - offset.top;
|
||||||
|
|
||||||
|
// send the computed touched axis to the zoom function so that it only zooms on that one
|
||||||
|
plot.zoom({
|
||||||
|
center: center,
|
||||||
|
amount: zoomAmount,
|
||||||
|
axes: navigationState.touchedAxis
|
||||||
|
});
|
||||||
|
gestureState.prevDistance = dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasPinchEvent(e, gestureState) {
|
||||||
|
return (gestureState.zoomEnable && e.detail.touches.length === 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAxis(plot, e, gesture, navigationState) {
|
||||||
|
if (e.type === 'pinchstart') {
|
||||||
|
var axisTouch1 = plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY);
|
||||||
|
var axisTouch2 = plot.getTouchedAxis(e.detail.touches[1].pageX, e.detail.touches[1].pageY);
|
||||||
|
|
||||||
|
if (axisTouch1.length === axisTouch2.length && axisTouch1.toString() === axisTouch2.toString()) {
|
||||||
|
return axisTouch1;
|
||||||
|
}
|
||||||
|
} else if (e.type === 'panstart') {
|
||||||
|
return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY);
|
||||||
|
} else if (e.type === 'pinchend') {
|
||||||
|
//update axis since instead on pinch, a pan event is made
|
||||||
|
return plot.getTouchedAxis(e.detail.touches[0].pageX, e.detail.touches[0].pageY);
|
||||||
|
} else {
|
||||||
|
return navigationState.touchedAxis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function noAxisTouched(navigationState) {
|
||||||
|
return (!navigationState.touchedAxis || navigationState.touchedAxis.length === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPrevDistance(e, gestureState) {
|
||||||
|
gestureState.prevDistance = pinchDistance(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateData(e, gesture, gestureState, navigationState) {
|
||||||
|
var axisDir,
|
||||||
|
point = getPoint(e, gesture);
|
||||||
|
|
||||||
|
switch (navigationState.navigationConstraint) {
|
||||||
|
case 'unconstrained':
|
||||||
|
navigationState.touchedAxis = null;
|
||||||
|
gestureState.prevTapPosition = {
|
||||||
|
x: gestureState.prevPanPosition.x,
|
||||||
|
y: gestureState.prevPanPosition.y
|
||||||
|
};
|
||||||
|
gestureState.prevPanPosition = {
|
||||||
|
x: point.x,
|
||||||
|
y: point.y
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'axisConstrained':
|
||||||
|
axisDir = navigationState.touchedAxis[0].direction;
|
||||||
|
navigationState.currentTouchedAxis = axisDir;
|
||||||
|
gestureState.prevTapPosition[axisDir] = gestureState.prevPanPosition[axisDir];
|
||||||
|
gestureState.prevPanPosition[axisDir] = point[axisDir];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(x1, y1, x2, y2) {
|
||||||
|
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pinchDistance(e) {
|
||||||
|
var t1 = e.detail.touches[0],
|
||||||
|
t2 = e.detail.touches[1];
|
||||||
|
return distance(t1.pageX, t1.pageY, t2.pageX, t2.pageY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePrevPanPosition(e, gesture, gestureState, navigationState) {
|
||||||
|
var point = getPoint(e, gesture);
|
||||||
|
|
||||||
|
switch (navigationState.navigationConstraint) {
|
||||||
|
case 'unconstrained':
|
||||||
|
gestureState.prevPanPosition.x = point.x;
|
||||||
|
gestureState.prevPanPosition.y = point.y;
|
||||||
|
break;
|
||||||
|
case 'axisConstrained':
|
||||||
|
gestureState.prevPanPosition[navigationState.currentTouchedAxis] =
|
||||||
|
point[navigationState.currentTouchedAxis];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delta(e, gesture, gestureState) {
|
||||||
|
var point = getPoint(e, gesture);
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: point.x - gestureState.prevPanPosition.x,
|
||||||
|
y: point.y - gestureState.prevPanPosition.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPoint(e, gesture) {
|
||||||
|
if (gesture === 'pinch') {
|
||||||
|
return {
|
||||||
|
x: (e.detail.touches[0].pageX + e.detail.touches[1].pageX) / 2,
|
||||||
|
y: (e.detail.touches[0].pageY + e.detail.touches[1].pageY) / 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
x: e.detail.touches[0].pageX,
|
||||||
|
y: e.detail.touches[0].pageY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(jQuery);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
(function ($) {
|
||||||
|
'use strict';
|
||||||
|
$.plot.uiConstants = {
|
||||||
|
SNAPPING_CONSTANT: 20,
|
||||||
|
PANHINT_LENGTH_CONSTANT: 10,
|
||||||
|
MINOR_TICKS_COUNT_CONSTANT: 4,
|
||||||
|
TICK_LENGTH_CONSTANT: 10,
|
||||||
|
ZOOM_DISTANCE_MARGIN: 25
|
||||||
|
};
|
||||||
|
})(jQuery);
|
||||||
|
After Width: | Height: | Size: 136 KiB |
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"org":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"name": "How to build a website"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "How to build a website 2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"name": "How to build a website 3"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
body {
|
||||||
|
background: rgb(2,0,36);
|
||||||
|
background: linear-gradient(138deg, rgba(2,0,36,1) 0%, rgba(5,5,209,1) 51%, rgba(0,212,255,1) 100%);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
#A {
|
||||||
|
background-color: #7e00db;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#B {
|
||||||
|
background-color: #2300ff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#C {
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#D {
|
||||||
|
background-color: #00eaff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#E {
|
||||||
|
background-color: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#F {
|
||||||
|
background-color: #70ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#G {
|
||||||
|
background-color: #c3ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#H {
|
||||||
|
background-color: #ffef00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#R {
|
||||||
|
background-color: #ff9b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
#I, #S {
|
||||||
|
background-color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#J {
|
||||||
|
background-color: #f60000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#T {
|
||||||
|
background-color: #c80000;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#U {
|
||||||
|
background-color: #8d0000;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-secondary {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flot-tick-label {
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<!-- Required meta tags -->
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
||||||
|
|
||||||
<!-- Bootstrap CSS -->
|
|
||||||
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<title>Spektrometer
|
|
||||||
</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1><div class="p-3 mb-2 bg-primary text-white">Spektrometer</div></h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button type="button" class="btn btn-dark btn-lg btn-block">Pridobi rezultate
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div id={{ chartID|safe }} class="chart" style="height: 100px; width: 500px"></div>
|
|
||||||
<script>
|
|
||||||
var chart_id = {{ chartID|safe }}
|
|
||||||
var series = {{ series|safe }}
|
|
||||||
var title = {{ title|safe }}
|
|
||||||
var xAxis = {{ xAxis|safe }}
|
|
||||||
var yAxis = {{ yAxis|safe }}
|
|
||||||
var chart = {{ chart|safe }}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Optional JavaScript -->
|
|
||||||
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
|
|
||||||
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
|
|
||||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
|
|
||||||
<script src="http://code.highcharts.com/highcharts.js"></script>
|
|
||||||
<script src="../static/graph.js"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script type = "text/javascript" src = "/static/bootstrap.min.js"></script>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#main.py - the main program file of the TeraHz project
|
|
||||||
#This code is licensed under 3-clause BSD licensed
|
|
||||||
import serial as ser
|
|
||||||
|
|
||||||
#global config
|
|
||||||
# TODO: move this to another file
|
|
||||||
uartpath = '/dev/ttyUSB0'
|
|
||||||
uartbaud = 115200
|
|
||||||
uarttout = 5
|
|
||||||
|
|
||||||
print('TeraHz project')
|
|
||||||
print('Accessing the serial port')
|
|
||||||
|
|
||||||
sp = 'blank'
|
|
||||||
|
|
||||||
try:
|
|
||||||
sp = ser.Serial(uartpath, uartbaud, timeout=uarttout)
|
|
||||||
except Exception as e:
|
|
||||||
print('Connection to serial port at {} failed!'.format(uartpath))
|
|
||||||
raise e
|
|
||||||
exit()
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
site_name: TeraHz Documentation
|
||||||
|
theme: readthedocs
|
||||||
|
nav:
|
||||||
|
- Start page: 'index.md'
|
||||||
|
- Advanced guides:
|
||||||
|
- Build guide: 'build.md'
|
||||||
|
- Developer's guide: 'dev-guide.md'
|
||||||
|
- Electrical connections: 'electrical.md'
|
||||||
|
- Latest dependencies: 'dependencies.md'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version: 2
|
||||||
|
mkdocs:
|
||||||
|
configuration: mkdocs.yml
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# getcdata.py - fetch the calibrated data from the AS7265x module
|
# getcdata.py - fetch the calibrated data from the AS7265x module
|
||||||
# this program is 3-clause BSD license
|
# All code in this file is licensed under the ISC license, provided in LICENSE.txt
|
||||||
|
|
||||||
import serial as ser
|
import serial as ser
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
# reset old environment variables
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
||||||
|
PATH="${_OLD_VIRTUAL_PATH:-}"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
||||||
|
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This should detect bash and zsh, which have a hash command that must
|
||||||
|
# be called to get it to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||||
|
hash -r
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
||||||
|
PS1="${_OLD_VIRTUAL_PS1:-}"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
if [ ! "$1" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV="/root/projekti/TeraHz/utils/venv"
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
||||||
|
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
||||||
|
if [ -n "${PYTHONHOME:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
|
if [ "x(venv) " != x ] ; then
|
||||||
|
PS1="(venv) ${PS1:-}"
|
||||||
|
else
|
||||||
|
if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
|
||||||
|
# special case for Aspen magic directories
|
||||||
|
# see http://www.zetadev.com/software/aspen/
|
||||||
|
PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
|
||||||
|
else
|
||||||
|
PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
export PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# This should detect bash and zsh, which have a hash command that must
|
||||||
|
# be called to get it to forget past commands. Without forgetting
|
||||||
|
# past commands the $PATH changes we made may not be respected
|
||||||
|
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
||||||
|
hash -r
|
||||||
|
fi
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV "/root/projekti/TeraHz/utils/venv"
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
|
if ("venv" != "") then
|
||||||
|
set env_name = "venv"
|
||||||
|
else
|
||||||
|
if (`basename "VIRTUAL_ENV"` == "__") then
|
||||||
|
# special case for Aspen magic directories
|
||||||
|
# see http://www.zetadev.com/software/aspen/
|
||||||
|
set env_name = `basename \`dirname "$VIRTUAL_ENV"\``
|
||||||
|
else
|
||||||
|
set env_name = `basename "$VIRTUAL_ENV"`
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
set prompt = "[$env_name] $prompt"
|
||||||
|
unset env_name
|
||||||
|
endif
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# This file must be used with ". bin/activate.fish" *from fish* (http://fishshell.org)
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
function deactivate -d "Exit virtualenv and return to normal shell environment"
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
functions -e fish_prompt
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
if test "$argv[1]" != "nondestructive"
|
||||||
|
# Self destruct!
|
||||||
|
functions -e deactivate
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV "/root/projekti/TeraHz/utils/venv"
|
||||||
|
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# fish uses a function instead of an env var to generate the prompt.
|
||||||
|
|
||||||
|
# save the current fish_prompt function as the function _old_fish_prompt
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
# with the original prompt function renamed, we can override with our own.
|
||||||
|
function fish_prompt
|
||||||
|
# Save the return status of the last command
|
||||||
|
set -l old_status $status
|
||||||
|
|
||||||
|
# Prompt override?
|
||||||
|
if test -n "(venv) "
|
||||||
|
printf "%s%s" "(venv) " (set_color normal)
|
||||||
|
else
|
||||||
|
# ...Otherwise, prepend env
|
||||||
|
set -l _checkbase (basename "$VIRTUAL_ENV")
|
||||||
|
if test $_checkbase = "__"
|
||||||
|
# special case for Aspen magic directories
|
||||||
|
# see http://www.zetadev.com/software/aspen/
|
||||||
|
printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal)
|
||||||
|
else
|
||||||
|
printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Restore the return status of the previous command.
|
||||||
|
echo "exit $old_status" | .
|
||||||
|
_old_fish_prompt
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
end
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from numpy.f2py.f2py2e import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from numpy.f2py.f2py2e import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from numpy.f2py.f2py2e import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,976 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
#
|
||||||
|
# Very simple serial terminal
|
||||||
|
#
|
||||||
|
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
||||||
|
# (C)2002-2015 Chris Liechti <cliechti@gmx.net>
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import serial
|
||||||
|
from serial.tools.list_ports import comports
|
||||||
|
from serial.tools import hexlify_codec
|
||||||
|
|
||||||
|
# pylint: disable=wrong-import-order,wrong-import-position
|
||||||
|
|
||||||
|
codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
raw_input
|
||||||
|
except NameError:
|
||||||
|
# pylint: disable=redefined-builtin,invalid-name
|
||||||
|
raw_input = input # in python3 it's "raw"
|
||||||
|
unichr = chr
|
||||||
|
|
||||||
|
|
||||||
|
def key_description(character):
|
||||||
|
"""generate a readable description for a key"""
|
||||||
|
ascii_code = ord(character)
|
||||||
|
if ascii_code < 32:
|
||||||
|
return 'Ctrl+{:c}'.format(ord('@') + ascii_code)
|
||||||
|
else:
|
||||||
|
return repr(character)
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
class ConsoleBase(object):
|
||||||
|
"""OS abstraction for console (input/output codec, no echo)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if sys.version_info >= (3, 0):
|
||||||
|
self.byte_output = sys.stdout.buffer
|
||||||
|
else:
|
||||||
|
self.byte_output = sys.stdout
|
||||||
|
self.output = sys.stdout
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
"""Set console to read single characters, no echo"""
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
"""Restore default console settings"""
|
||||||
|
|
||||||
|
def getkey(self):
|
||||||
|
"""Read a single key from the console"""
|
||||||
|
return None
|
||||||
|
|
||||||
|
def write_bytes(self, byte_string):
|
||||||
|
"""Write bytes (already encoded)"""
|
||||||
|
self.byte_output.write(byte_string)
|
||||||
|
self.byte_output.flush()
|
||||||
|
|
||||||
|
def write(self, text):
|
||||||
|
"""Write string"""
|
||||||
|
self.output.write(text)
|
||||||
|
self.output.flush()
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
"""Cancel getkey operation"""
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# context manager:
|
||||||
|
# switch terminal temporary to normal mode (e.g. to get user input)
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.cleanup()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args, **kwargs):
|
||||||
|
self.setup()
|
||||||
|
|
||||||
|
|
||||||
|
if os.name == 'nt': # noqa
|
||||||
|
import msvcrt
|
||||||
|
import ctypes
|
||||||
|
|
||||||
|
class Out(object):
|
||||||
|
"""file-like wrapper that uses os.write"""
|
||||||
|
|
||||||
|
def __init__(self, fd):
|
||||||
|
self.fd = fd
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, s):
|
||||||
|
os.write(self.fd, s)
|
||||||
|
|
||||||
|
class Console(ConsoleBase):
|
||||||
|
def __init__(self):
|
||||||
|
super(Console, self).__init__()
|
||||||
|
self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
|
||||||
|
self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
|
||||||
|
ctypes.windll.kernel32.SetConsoleOutputCP(65001)
|
||||||
|
ctypes.windll.kernel32.SetConsoleCP(65001)
|
||||||
|
self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
|
||||||
|
# the change of the code page is not propagated to Python, manually fix it
|
||||||
|
sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
|
||||||
|
sys.stdout = self.output
|
||||||
|
self.output.encoding = 'UTF-8' # needed for input
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
|
||||||
|
ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
|
||||||
|
|
||||||
|
def getkey(self):
|
||||||
|
while True:
|
||||||
|
z = msvcrt.getwch()
|
||||||
|
if z == unichr(13):
|
||||||
|
return unichr(10)
|
||||||
|
elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore
|
||||||
|
msvcrt.getwch()
|
||||||
|
else:
|
||||||
|
return z
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
# CancelIo, CancelSynchronousIo do not seem to work when using
|
||||||
|
# getwch, so instead, send a key to the window with the console
|
||||||
|
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
|
||||||
|
ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
|
||||||
|
|
||||||
|
elif os.name == 'posix':
|
||||||
|
import atexit
|
||||||
|
import termios
|
||||||
|
import fcntl
|
||||||
|
|
||||||
|
class Console(ConsoleBase):
|
||||||
|
def __init__(self):
|
||||||
|
super(Console, self).__init__()
|
||||||
|
self.fd = sys.stdin.fileno()
|
||||||
|
self.old = termios.tcgetattr(self.fd)
|
||||||
|
atexit.register(self.cleanup)
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
|
||||||
|
else:
|
||||||
|
self.enc_stdin = sys.stdin
|
||||||
|
|
||||||
|
def setup(self):
|
||||||
|
new = termios.tcgetattr(self.fd)
|
||||||
|
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
|
||||||
|
new[6][termios.VMIN] = 1
|
||||||
|
new[6][termios.VTIME] = 0
|
||||||
|
termios.tcsetattr(self.fd, termios.TCSANOW, new)
|
||||||
|
|
||||||
|
def getkey(self):
|
||||||
|
c = self.enc_stdin.read(1)
|
||||||
|
if c == unichr(0x7f):
|
||||||
|
c = unichr(8) # map the BS key (which yields DEL) to backspace
|
||||||
|
return c
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
|
||||||
|
|
||||||
|
def cleanup(self):
|
||||||
|
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
|
||||||
|
class Transform(object):
|
||||||
|
"""do-nothing: forward all data unchanged"""
|
||||||
|
def rx(self, text):
|
||||||
|
"""text received from serial port"""
|
||||||
|
return text
|
||||||
|
|
||||||
|
def tx(self, text):
|
||||||
|
"""text to be sent to serial port"""
|
||||||
|
return text
|
||||||
|
|
||||||
|
def echo(self, text):
|
||||||
|
"""text to be sent but displayed on console"""
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
class CRLF(Transform):
|
||||||
|
"""ENTER sends CR+LF"""
|
||||||
|
|
||||||
|
def tx(self, text):
|
||||||
|
return text.replace('\n', '\r\n')
|
||||||
|
|
||||||
|
|
||||||
|
class CR(Transform):
|
||||||
|
"""ENTER sends CR"""
|
||||||
|
|
||||||
|
def rx(self, text):
|
||||||
|
return text.replace('\r', '\n')
|
||||||
|
|
||||||
|
def tx(self, text):
|
||||||
|
return text.replace('\n', '\r')
|
||||||
|
|
||||||
|
|
||||||
|
class LF(Transform):
|
||||||
|
"""ENTER sends LF"""
|
||||||
|
|
||||||
|
|
||||||
|
class NoTerminal(Transform):
|
||||||
|
"""remove typical terminal control codes from input"""
|
||||||
|
|
||||||
|
REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t')
|
||||||
|
REPLACEMENT_MAP.update(
|
||||||
|
{
|
||||||
|
0x7F: 0x2421, # DEL
|
||||||
|
0x9B: 0x2425, # CSI
|
||||||
|
})
|
||||||
|
|
||||||
|
def rx(self, text):
|
||||||
|
return text.translate(self.REPLACEMENT_MAP)
|
||||||
|
|
||||||
|
echo = rx
|
||||||
|
|
||||||
|
|
||||||
|
class NoControls(NoTerminal):
|
||||||
|
"""Remove all control codes, incl. CR+LF"""
|
||||||
|
|
||||||
|
REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
|
||||||
|
REPLACEMENT_MAP.update(
|
||||||
|
{
|
||||||
|
0x20: 0x2423, # visual space
|
||||||
|
0x7F: 0x2421, # DEL
|
||||||
|
0x9B: 0x2425, # CSI
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class Printable(Transform):
|
||||||
|
"""Show decimal code for all non-ASCII characters and replace most control codes"""
|
||||||
|
|
||||||
|
def rx(self, text):
|
||||||
|
r = []
|
||||||
|
for c in text:
|
||||||
|
if ' ' <= c < '\x7f' or c in '\r\n\b\t':
|
||||||
|
r.append(c)
|
||||||
|
elif c < ' ':
|
||||||
|
r.append(unichr(0x2400 + ord(c)))
|
||||||
|
else:
|
||||||
|
r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
|
||||||
|
r.append(' ')
|
||||||
|
return ''.join(r)
|
||||||
|
|
||||||
|
echo = rx
|
||||||
|
|
||||||
|
|
||||||
|
class Colorize(Transform):
|
||||||
|
"""Apply different colors for received and echo"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
# XXX make it configurable, use colorama?
|
||||||
|
self.input_color = '\x1b[37m'
|
||||||
|
self.echo_color = '\x1b[31m'
|
||||||
|
|
||||||
|
def rx(self, text):
|
||||||
|
return self.input_color + text
|
||||||
|
|
||||||
|
def echo(self, text):
|
||||||
|
return self.echo_color + text
|
||||||
|
|
||||||
|
|
||||||
|
class DebugIO(Transform):
|
||||||
|
"""Print what is sent and received"""
|
||||||
|
|
||||||
|
def rx(self, text):
|
||||||
|
sys.stderr.write(' [RX:{}] '.format(repr(text)))
|
||||||
|
sys.stderr.flush()
|
||||||
|
return text
|
||||||
|
|
||||||
|
def tx(self, text):
|
||||||
|
sys.stderr.write(' [TX:{}] '.format(repr(text)))
|
||||||
|
sys.stderr.flush()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
# other ideas:
|
||||||
|
# - add date/time for each newline
|
||||||
|
# - insert newline after: a) timeout b) packet end character
|
||||||
|
|
||||||
|
EOL_TRANSFORMATIONS = {
|
||||||
|
'crlf': CRLF,
|
||||||
|
'cr': CR,
|
||||||
|
'lf': LF,
|
||||||
|
}
|
||||||
|
|
||||||
|
TRANSFORMATIONS = {
|
||||||
|
'direct': Transform, # no transformation
|
||||||
|
'default': NoTerminal,
|
||||||
|
'nocontrol': NoControls,
|
||||||
|
'printable': Printable,
|
||||||
|
'colorize': Colorize,
|
||||||
|
'debug': DebugIO,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
def ask_for_port():
|
||||||
|
"""\
|
||||||
|
Show a list of ports and ask the user for a choice. To make selection
|
||||||
|
easier on systems with long device names, also allow the input of an
|
||||||
|
index.
|
||||||
|
"""
|
||||||
|
sys.stderr.write('\n--- Available ports:\n')
|
||||||
|
ports = []
|
||||||
|
for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
|
||||||
|
sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
|
||||||
|
ports.append(port)
|
||||||
|
while True:
|
||||||
|
port = raw_input('--- Enter port index or full name: ')
|
||||||
|
try:
|
||||||
|
index = int(port) - 1
|
||||||
|
if not 0 <= index < len(ports):
|
||||||
|
sys.stderr.write('--- Invalid index!\n')
|
||||||
|
continue
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
port = ports[index]
|
||||||
|
return port
|
||||||
|
|
||||||
|
|
||||||
|
class Miniterm(object):
|
||||||
|
"""\
|
||||||
|
Terminal application. Copy data from serial port to console and vice versa.
|
||||||
|
Handle special keys from the console to show menu etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
|
||||||
|
self.console = Console()
|
||||||
|
self.serial = serial_instance
|
||||||
|
self.echo = echo
|
||||||
|
self.raw = False
|
||||||
|
self.input_encoding = 'UTF-8'
|
||||||
|
self.output_encoding = 'UTF-8'
|
||||||
|
self.eol = eol
|
||||||
|
self.filters = filters
|
||||||
|
self.update_transformations()
|
||||||
|
self.exit_character = 0x1d # GS/CTRL+]
|
||||||
|
self.menu_character = 0x14 # Menu: CTRL+T
|
||||||
|
self.alive = None
|
||||||
|
self._reader_alive = None
|
||||||
|
self.receiver_thread = None
|
||||||
|
self.rx_decoder = None
|
||||||
|
self.tx_decoder = None
|
||||||
|
|
||||||
|
def _start_reader(self):
|
||||||
|
"""Start reader thread"""
|
||||||
|
self._reader_alive = True
|
||||||
|
# start serial->console thread
|
||||||
|
self.receiver_thread = threading.Thread(target=self.reader, name='rx')
|
||||||
|
self.receiver_thread.daemon = True
|
||||||
|
self.receiver_thread.start()
|
||||||
|
|
||||||
|
def _stop_reader(self):
|
||||||
|
"""Stop reader thread only, wait for clean exit of thread"""
|
||||||
|
self._reader_alive = False
|
||||||
|
if hasattr(self.serial, 'cancel_read'):
|
||||||
|
self.serial.cancel_read()
|
||||||
|
self.receiver_thread.join()
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""start worker threads"""
|
||||||
|
self.alive = True
|
||||||
|
self._start_reader()
|
||||||
|
# enter console->serial loop
|
||||||
|
self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
|
||||||
|
self.transmitter_thread.daemon = True
|
||||||
|
self.transmitter_thread.start()
|
||||||
|
self.console.setup()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""set flag to stop worker threads"""
|
||||||
|
self.alive = False
|
||||||
|
|
||||||
|
def join(self, transmit_only=False):
|
||||||
|
"""wait for worker threads to terminate"""
|
||||||
|
self.transmitter_thread.join()
|
||||||
|
if not transmit_only:
|
||||||
|
if hasattr(self.serial, 'cancel_read'):
|
||||||
|
self.serial.cancel_read()
|
||||||
|
self.receiver_thread.join()
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.serial.close()
|
||||||
|
|
||||||
|
def update_transformations(self):
|
||||||
|
"""take list of transformation classes and instantiate them for rx and tx"""
|
||||||
|
transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
|
||||||
|
for f in self.filters]
|
||||||
|
self.tx_transformations = [t() for t in transformations]
|
||||||
|
self.rx_transformations = list(reversed(self.tx_transformations))
|
||||||
|
|
||||||
|
def set_rx_encoding(self, encoding, errors='replace'):
|
||||||
|
"""set encoding for received data"""
|
||||||
|
self.input_encoding = encoding
|
||||||
|
self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
|
||||||
|
|
||||||
|
def set_tx_encoding(self, encoding, errors='replace'):
|
||||||
|
"""set encoding for transmitted data"""
|
||||||
|
self.output_encoding = encoding
|
||||||
|
self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
|
||||||
|
|
||||||
|
def dump_port_settings(self):
|
||||||
|
"""Write current settings to sys.stderr"""
|
||||||
|
sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
|
||||||
|
p=self.serial))
|
||||||
|
sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
|
||||||
|
('active' if self.serial.rts else 'inactive'),
|
||||||
|
('active' if self.serial.dtr else 'inactive'),
|
||||||
|
('active' if self.serial.break_condition else 'inactive')))
|
||||||
|
try:
|
||||||
|
sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
|
||||||
|
('active' if self.serial.cts else 'inactive'),
|
||||||
|
('active' if self.serial.dsr else 'inactive'),
|
||||||
|
('active' if self.serial.ri else 'inactive'),
|
||||||
|
('active' if self.serial.cd else 'inactive')))
|
||||||
|
except serial.SerialException:
|
||||||
|
# on RFC 2217 ports, it can happen if no modem state notification was
|
||||||
|
# yet received. ignore this error.
|
||||||
|
pass
|
||||||
|
sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
|
||||||
|
sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
|
||||||
|
sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
|
||||||
|
sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
|
||||||
|
sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
|
||||||
|
sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
|
||||||
|
|
||||||
|
def reader(self):
|
||||||
|
"""loop and copy serial->console"""
|
||||||
|
try:
|
||||||
|
while self.alive and self._reader_alive:
|
||||||
|
# read all that is there or wait for one byte
|
||||||
|
data = self.serial.read(self.serial.in_waiting or 1)
|
||||||
|
if data:
|
||||||
|
if self.raw:
|
||||||
|
self.console.write_bytes(data)
|
||||||
|
else:
|
||||||
|
text = self.rx_decoder.decode(data)
|
||||||
|
for transformation in self.rx_transformations:
|
||||||
|
text = transformation.rx(text)
|
||||||
|
self.console.write(text)
|
||||||
|
except serial.SerialException:
|
||||||
|
self.alive = False
|
||||||
|
self.console.cancel()
|
||||||
|
raise # XXX handle instead of re-raise?
|
||||||
|
|
||||||
|
def writer(self):
|
||||||
|
"""\
|
||||||
|
Loop and copy console->serial until self.exit_character character is
|
||||||
|
found. When self.menu_character is found, interpret the next key
|
||||||
|
locally.
|
||||||
|
"""
|
||||||
|
menu_active = False
|
||||||
|
try:
|
||||||
|
while self.alive:
|
||||||
|
try:
|
||||||
|
c = self.console.getkey()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
c = '\x03'
|
||||||
|
if not self.alive:
|
||||||
|
break
|
||||||
|
if menu_active:
|
||||||
|
self.handle_menu_key(c)
|
||||||
|
menu_active = False
|
||||||
|
elif c == self.menu_character:
|
||||||
|
menu_active = True # next char will be for menu
|
||||||
|
elif c == self.exit_character:
|
||||||
|
self.stop() # exit app
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
#~ if self.raw:
|
||||||
|
text = c
|
||||||
|
for transformation in self.tx_transformations:
|
||||||
|
text = transformation.tx(text)
|
||||||
|
self.serial.write(self.tx_encoder.encode(text))
|
||||||
|
if self.echo:
|
||||||
|
echo_text = c
|
||||||
|
for transformation in self.tx_transformations:
|
||||||
|
echo_text = transformation.echo(echo_text)
|
||||||
|
self.console.write(echo_text)
|
||||||
|
except:
|
||||||
|
self.alive = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
def handle_menu_key(self, c):
|
||||||
|
"""Implement a simple menu / settings"""
|
||||||
|
if c == self.menu_character or c == self.exit_character:
|
||||||
|
# Menu/exit character again -> send itself
|
||||||
|
self.serial.write(self.tx_encoder.encode(c))
|
||||||
|
if self.echo:
|
||||||
|
self.console.write(c)
|
||||||
|
elif c == '\x15': # CTRL+U -> upload file
|
||||||
|
self.upload_file()
|
||||||
|
elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
|
||||||
|
sys.stderr.write(self.get_help_text())
|
||||||
|
elif c == '\x12': # CTRL+R -> Toggle RTS
|
||||||
|
self.serial.rts = not self.serial.rts
|
||||||
|
sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
|
||||||
|
elif c == '\x04': # CTRL+D -> Toggle DTR
|
||||||
|
self.serial.dtr = not self.serial.dtr
|
||||||
|
sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
|
||||||
|
elif c == '\x02': # CTRL+B -> toggle BREAK condition
|
||||||
|
self.serial.break_condition = not self.serial.break_condition
|
||||||
|
sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
|
||||||
|
elif c == '\x05': # CTRL+E -> toggle local echo
|
||||||
|
self.echo = not self.echo
|
||||||
|
sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
|
||||||
|
elif c == '\x06': # CTRL+F -> edit filters
|
||||||
|
self.change_filter()
|
||||||
|
elif c == '\x0c': # CTRL+L -> EOL mode
|
||||||
|
modes = list(EOL_TRANSFORMATIONS) # keys
|
||||||
|
eol = modes.index(self.eol) + 1
|
||||||
|
if eol >= len(modes):
|
||||||
|
eol = 0
|
||||||
|
self.eol = modes[eol]
|
||||||
|
sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
|
||||||
|
self.update_transformations()
|
||||||
|
elif c == '\x01': # CTRL+A -> set encoding
|
||||||
|
self.change_encoding()
|
||||||
|
elif c == '\x09': # CTRL+I -> info
|
||||||
|
self.dump_port_settings()
|
||||||
|
#~ elif c == '\x01': # CTRL+A -> cycle escape mode
|
||||||
|
#~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
|
||||||
|
elif c in 'pP': # P -> change port
|
||||||
|
self.change_port()
|
||||||
|
elif c in 'sS': # S -> suspend / open port temporarily
|
||||||
|
self.suspend_port()
|
||||||
|
elif c in 'bB': # B -> change baudrate
|
||||||
|
self.change_baudrate()
|
||||||
|
elif c == '8': # 8 -> change to 8 bits
|
||||||
|
self.serial.bytesize = serial.EIGHTBITS
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c == '7': # 7 -> change to 8 bits
|
||||||
|
self.serial.bytesize = serial.SEVENBITS
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'eE': # E -> change to even parity
|
||||||
|
self.serial.parity = serial.PARITY_EVEN
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'oO': # O -> change to odd parity
|
||||||
|
self.serial.parity = serial.PARITY_ODD
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'mM': # M -> change to mark parity
|
||||||
|
self.serial.parity = serial.PARITY_MARK
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'sS': # S -> change to space parity
|
||||||
|
self.serial.parity = serial.PARITY_SPACE
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'nN': # N -> change to no parity
|
||||||
|
self.serial.parity = serial.PARITY_NONE
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c == '1': # 1 -> change to 1 stop bits
|
||||||
|
self.serial.stopbits = serial.STOPBITS_ONE
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c == '2': # 2 -> change to 2 stop bits
|
||||||
|
self.serial.stopbits = serial.STOPBITS_TWO
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c == '3': # 3 -> change to 1.5 stop bits
|
||||||
|
self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'xX': # X -> change software flow control
|
||||||
|
self.serial.xonxoff = (c == 'X')
|
||||||
|
self.dump_port_settings()
|
||||||
|
elif c in 'rR': # R -> change hardware flow control
|
||||||
|
self.serial.rtscts = (c == 'R')
|
||||||
|
self.dump_port_settings()
|
||||||
|
else:
|
||||||
|
sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
|
||||||
|
|
||||||
|
def upload_file(self):
|
||||||
|
"""Ask user for filenname and send its contents"""
|
||||||
|
sys.stderr.write('\n--- File to upload: ')
|
||||||
|
sys.stderr.flush()
|
||||||
|
with self.console:
|
||||||
|
filename = sys.stdin.readline().rstrip('\r\n')
|
||||||
|
if filename:
|
||||||
|
try:
|
||||||
|
with open(filename, 'rb') as f:
|
||||||
|
sys.stderr.write('--- Sending file {} ---\n'.format(filename))
|
||||||
|
while True:
|
||||||
|
block = f.read(1024)
|
||||||
|
if not block:
|
||||||
|
break
|
||||||
|
self.serial.write(block)
|
||||||
|
# Wait for output buffer to drain.
|
||||||
|
self.serial.flush()
|
||||||
|
sys.stderr.write('.') # Progress indicator.
|
||||||
|
sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
|
||||||
|
except IOError as e:
|
||||||
|
sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
|
||||||
|
|
||||||
|
def change_filter(self):
|
||||||
|
"""change the i/o transformations"""
|
||||||
|
sys.stderr.write('\n--- Available Filters:\n')
|
||||||
|
sys.stderr.write('\n'.join(
|
||||||
|
'--- {:<10} = {.__doc__}'.format(k, v)
|
||||||
|
for k, v in sorted(TRANSFORMATIONS.items())))
|
||||||
|
sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
|
||||||
|
with self.console:
|
||||||
|
new_filters = sys.stdin.readline().lower().split()
|
||||||
|
if new_filters:
|
||||||
|
for f in new_filters:
|
||||||
|
if f not in TRANSFORMATIONS:
|
||||||
|
sys.stderr.write('--- unknown filter: {}\n'.format(repr(f)))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.filters = new_filters
|
||||||
|
self.update_transformations()
|
||||||
|
sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
|
||||||
|
|
||||||
|
def change_encoding(self):
|
||||||
|
"""change encoding on the serial port"""
|
||||||
|
sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
|
||||||
|
with self.console:
|
||||||
|
new_encoding = sys.stdin.readline().strip()
|
||||||
|
if new_encoding:
|
||||||
|
try:
|
||||||
|
codecs.lookup(new_encoding)
|
||||||
|
except LookupError:
|
||||||
|
sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
|
||||||
|
else:
|
||||||
|
self.set_rx_encoding(new_encoding)
|
||||||
|
self.set_tx_encoding(new_encoding)
|
||||||
|
sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
|
||||||
|
sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
|
||||||
|
|
||||||
|
def change_baudrate(self):
|
||||||
|
"""change the baudrate"""
|
||||||
|
sys.stderr.write('\n--- Baudrate: ')
|
||||||
|
sys.stderr.flush()
|
||||||
|
with self.console:
|
||||||
|
backup = self.serial.baudrate
|
||||||
|
try:
|
||||||
|
self.serial.baudrate = int(sys.stdin.readline().strip())
|
||||||
|
except ValueError as e:
|
||||||
|
sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
|
||||||
|
self.serial.baudrate = backup
|
||||||
|
else:
|
||||||
|
self.dump_port_settings()
|
||||||
|
|
||||||
|
def change_port(self):
|
||||||
|
"""Have a conversation with the user to change the serial port"""
|
||||||
|
with self.console:
|
||||||
|
try:
|
||||||
|
port = ask_for_port()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
port = None
|
||||||
|
if port and port != self.serial.port:
|
||||||
|
# reader thread needs to be shut down
|
||||||
|
self._stop_reader()
|
||||||
|
# save settings
|
||||||
|
settings = self.serial.getSettingsDict()
|
||||||
|
try:
|
||||||
|
new_serial = serial.serial_for_url(port, do_not_open=True)
|
||||||
|
# restore settings and open
|
||||||
|
new_serial.applySettingsDict(settings)
|
||||||
|
new_serial.rts = self.serial.rts
|
||||||
|
new_serial.dtr = self.serial.dtr
|
||||||
|
new_serial.open()
|
||||||
|
new_serial.break_condition = self.serial.break_condition
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
|
||||||
|
new_serial.close()
|
||||||
|
else:
|
||||||
|
self.serial.close()
|
||||||
|
self.serial = new_serial
|
||||||
|
sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
|
||||||
|
# and restart the reader thread
|
||||||
|
self._start_reader()
|
||||||
|
|
||||||
|
def suspend_port(self):
|
||||||
|
"""\
|
||||||
|
open port temporarily, allow reconnect, exit and port change to get
|
||||||
|
out of the loop
|
||||||
|
"""
|
||||||
|
# reader thread needs to be shut down
|
||||||
|
self._stop_reader()
|
||||||
|
self.serial.close()
|
||||||
|
sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
|
||||||
|
do_change_port = False
|
||||||
|
while not self.serial.is_open:
|
||||||
|
sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
|
||||||
|
exit=key_description(self.exit_character)))
|
||||||
|
k = self.console.getkey()
|
||||||
|
if k == self.exit_character:
|
||||||
|
self.stop() # exit app
|
||||||
|
break
|
||||||
|
elif k in 'pP':
|
||||||
|
do_change_port = True
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
self.serial.open()
|
||||||
|
except Exception as e:
|
||||||
|
sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
|
||||||
|
if do_change_port:
|
||||||
|
self.change_port()
|
||||||
|
else:
|
||||||
|
# and restart the reader thread
|
||||||
|
self._start_reader()
|
||||||
|
sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
"""return the help text"""
|
||||||
|
# help text, starts with blank line!
|
||||||
|
return """
|
||||||
|
--- pySerial ({version}) - miniterm - help
|
||||||
|
---
|
||||||
|
--- {exit:8} Exit program
|
||||||
|
--- {menu:8} Menu escape key, followed by:
|
||||||
|
--- Menu keys:
|
||||||
|
--- {menu:7} Send the menu character itself to remote
|
||||||
|
--- {exit:7} Send the exit character itself to remote
|
||||||
|
--- {info:7} Show info
|
||||||
|
--- {upload:7} Upload file (prompt will be shown)
|
||||||
|
--- {repr:7} encoding
|
||||||
|
--- {filter:7} edit filters
|
||||||
|
--- Toggles:
|
||||||
|
--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
|
||||||
|
--- {echo:7} echo {eol:7} EOL
|
||||||
|
---
|
||||||
|
--- Port settings ({menu} followed by the following):
|
||||||
|
--- p change port
|
||||||
|
--- 7 8 set data bits
|
||||||
|
--- N E O S M change parity (None, Even, Odd, Space, Mark)
|
||||||
|
--- 1 2 3 set stop bits (1, 2, 1.5)
|
||||||
|
--- b change baud rate
|
||||||
|
--- x X disable/enable software flow control
|
||||||
|
--- r R disable/enable hardware flow control
|
||||||
|
""".format(version=getattr(serial, 'VERSION', 'unknown version'),
|
||||||
|
exit=key_description(self.exit_character),
|
||||||
|
menu=key_description(self.menu_character),
|
||||||
|
rts=key_description('\x12'),
|
||||||
|
dtr=key_description('\x04'),
|
||||||
|
brk=key_description('\x02'),
|
||||||
|
echo=key_description('\x05'),
|
||||||
|
info=key_description('\x09'),
|
||||||
|
upload=key_description('\x15'),
|
||||||
|
repr=key_description('\x01'),
|
||||||
|
filter=key_description('\x06'),
|
||||||
|
eol=key_description('\x0c'))
|
||||||
|
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
# default args can be used to override when calling main() from an other script
|
||||||
|
# e.g to create a miniterm-my-device.py
|
||||||
|
def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
|
||||||
|
"""Command line tool, entry point"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Miniterm - A simple terminal program for the serial port.")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"port",
|
||||||
|
nargs='?',
|
||||||
|
help="serial port name ('-' to show port list)",
|
||||||
|
default=default_port)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"baudrate",
|
||||||
|
nargs='?',
|
||||||
|
type=int,
|
||||||
|
help="set baud rate, default: %(default)s",
|
||||||
|
default=default_baudrate)
|
||||||
|
|
||||||
|
group = parser.add_argument_group("port settings")
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--parity",
|
||||||
|
choices=['N', 'E', 'O', 'S', 'M'],
|
||||||
|
type=lambda c: c.upper(),
|
||||||
|
help="set parity, one of {N E O S M}, default: N",
|
||||||
|
default='N')
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--rtscts",
|
||||||
|
action="store_true",
|
||||||
|
help="enable RTS/CTS flow control (default off)",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--xonxoff",
|
||||||
|
action="store_true",
|
||||||
|
help="enable software flow control (default off)",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--rts",
|
||||||
|
type=int,
|
||||||
|
help="set initial RTS line state (possible values: 0, 1)",
|
||||||
|
default=default_rts)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--dtr",
|
||||||
|
type=int,
|
||||||
|
help="set initial DTR line state (possible values: 0, 1)",
|
||||||
|
default=default_dtr)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--ask",
|
||||||
|
action="store_true",
|
||||||
|
help="ask again for port when open fails",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
group = parser.add_argument_group("data handling")
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"-e", "--echo",
|
||||||
|
action="store_true",
|
||||||
|
help="enable local echo (default off)",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--encoding",
|
||||||
|
dest="serial_port_encoding",
|
||||||
|
metavar="CODEC",
|
||||||
|
help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
|
||||||
|
default='UTF-8')
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"-f", "--filter",
|
||||||
|
action="append",
|
||||||
|
metavar="NAME",
|
||||||
|
help="add text transformation",
|
||||||
|
default=[])
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--eol",
|
||||||
|
choices=['CR', 'LF', 'CRLF'],
|
||||||
|
type=lambda c: c.upper(),
|
||||||
|
help="end of line mode",
|
||||||
|
default='CRLF')
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--raw",
|
||||||
|
action="store_true",
|
||||||
|
help="Do no apply any encodings/transformations",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
group = parser.add_argument_group("hotkeys")
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--exit-char",
|
||||||
|
type=int,
|
||||||
|
metavar='NUM',
|
||||||
|
help="Unicode of special character that is used to exit the application, default: %(default)s",
|
||||||
|
default=0x1d) # GS/CTRL+]
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--menu-char",
|
||||||
|
type=int,
|
||||||
|
metavar='NUM',
|
||||||
|
help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
|
||||||
|
default=0x14) # Menu: CTRL+T
|
||||||
|
|
||||||
|
group = parser.add_argument_group("diagnostics")
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"-q", "--quiet",
|
||||||
|
action="store_true",
|
||||||
|
help="suppress non-error messages",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
group.add_argument(
|
||||||
|
"--develop",
|
||||||
|
action="store_true",
|
||||||
|
help="show Python traceback on error",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.menu_char == args.exit_char:
|
||||||
|
parser.error('--exit-char can not be the same as --menu-char')
|
||||||
|
|
||||||
|
if args.filter:
|
||||||
|
if 'help' in args.filter:
|
||||||
|
sys.stderr.write('Available filters:\n')
|
||||||
|
sys.stderr.write('\n'.join(
|
||||||
|
'{:<10} = {.__doc__}'.format(k, v)
|
||||||
|
for k, v in sorted(TRANSFORMATIONS.items())))
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
sys.exit(1)
|
||||||
|
filters = args.filter
|
||||||
|
else:
|
||||||
|
filters = ['default']
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# no port given on command line -> ask user now
|
||||||
|
if args.port is None or args.port == '-':
|
||||||
|
try:
|
||||||
|
args.port = ask_for_port()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.stderr.write('\n')
|
||||||
|
parser.error('user aborted and port is not given')
|
||||||
|
else:
|
||||||
|
if not args.port:
|
||||||
|
parser.error('port is not given')
|
||||||
|
try:
|
||||||
|
serial_instance = serial.serial_for_url(
|
||||||
|
args.port,
|
||||||
|
args.baudrate,
|
||||||
|
parity=args.parity,
|
||||||
|
rtscts=args.rtscts,
|
||||||
|
xonxoff=args.xonxoff,
|
||||||
|
do_not_open=True)
|
||||||
|
|
||||||
|
if not hasattr(serial_instance, 'cancel_read'):
|
||||||
|
# enable timeout for alive flag polling if cancel_read is not available
|
||||||
|
serial_instance.timeout = 1
|
||||||
|
|
||||||
|
if args.dtr is not None:
|
||||||
|
if not args.quiet:
|
||||||
|
sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
|
||||||
|
serial_instance.dtr = args.dtr
|
||||||
|
if args.rts is not None:
|
||||||
|
if not args.quiet:
|
||||||
|
sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
|
||||||
|
serial_instance.rts = args.rts
|
||||||
|
|
||||||
|
serial_instance.open()
|
||||||
|
except serial.SerialException as e:
|
||||||
|
sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
|
||||||
|
if args.develop:
|
||||||
|
raise
|
||||||
|
if not args.ask:
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
args.port = '-'
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
miniterm = Miniterm(
|
||||||
|
serial_instance,
|
||||||
|
echo=args.echo,
|
||||||
|
eol=args.eol.lower(),
|
||||||
|
filters=filters)
|
||||||
|
miniterm.exit_character = unichr(args.exit_char)
|
||||||
|
miniterm.menu_character = unichr(args.menu_char)
|
||||||
|
miniterm.raw = args.raw
|
||||||
|
miniterm.set_rx_encoding(args.serial_port_encoding)
|
||||||
|
miniterm.set_tx_encoding(args.serial_port_encoding)
|
||||||
|
|
||||||
|
if not args.quiet:
|
||||||
|
sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
|
||||||
|
p=miniterm.serial))
|
||||||
|
sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
|
||||||
|
key_description(miniterm.exit_character),
|
||||||
|
key_description(miniterm.menu_character),
|
||||||
|
key_description(miniterm.menu_character),
|
||||||
|
key_description('\x08')))
|
||||||
|
|
||||||
|
miniterm.start()
|
||||||
|
try:
|
||||||
|
miniterm.join(True)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
|
if not args.quiet:
|
||||||
|
sys.stderr.write("\n--- exit ---\n")
|
||||||
|
miniterm.join()
|
||||||
|
miniterm.close()
|
||||||
|
|
||||||
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pip._internal import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pip._internal import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from pip._internal import main
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
Metadata-Version: 2.0
|
||||||
|
Name: cycler
|
||||||
|
Version: 0.10.0
|
||||||
|
Summary: Composable style cycles
|
||||||
|
Home-page: http://github.com/matplotlib/cycler
|
||||||
|
Author: Thomas A Caswell
|
||||||
|
Author-email: matplotlib-users@python.org
|
||||||
|
License: BSD
|
||||||
|
Keywords: cycle kwargs
|
||||||
|
Platform: Cross platform (Linux
|
||||||
|
Platform: Mac OSX
|
||||||
|
Platform: Windows)
|
||||||
|
Classifier: Development Status :: 4 - Beta
|
||||||
|
Classifier: Programming Language :: Python :: 2
|
||||||
|
Classifier: Programming Language :: Python :: 2.6
|
||||||
|
Classifier: Programming Language :: Python :: 2.7
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.3
|
||||||
|
Classifier: Programming Language :: Python :: 3.4
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Requires-Dist: six
|
||||||
|
|
||||||
|
UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/cycler.cpython-36.pyc,,
|
||||||
|
cycler-0.10.0.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10
|
||||||
|
cycler-0.10.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
cycler-0.10.0.dist-info/METADATA,sha256=aWX1pyo7D2hSDNZ2Q6Zl7DxhUQdpyu1O5uNABnvz000,722
|
||||||
|
cycler-0.10.0.dist-info/RECORD,,
|
||||||
|
cycler-0.10.0.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
|
||||||
|
cycler-0.10.0.dist-info/metadata.json,sha256=CCBpg-KQU-VRL1unJcHPWKQeQbB84G0j7-BeCj7YUbU,875
|
||||||
|
cycler-0.10.0.dist-info/top_level.txt,sha256=D8BVVDdAAelLb2FOEz7lDpc6-AL21ylKPrMhtG6yzyE,7
|
||||||
|
cycler.py,sha256=ed3G39unvVEBrBZVDwnE0FFroRNsOLkbJ_TwIT5CjCU,15959
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.29.0)
|
||||||
|
Root-Is-Purelib: true
|
||||||
|
Tag: py2-none-any
|
||||||
|
Tag: py3-none-any
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"classifiers": ["Development Status :: 4 - Beta", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "extensions": {"python.details": {"contacts": [{"email": "matplotlib-users@python.org", "name": "Thomas A Caswell", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/matplotlib/cycler"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "keywords": ["cycle", "kwargs"], "license": "BSD", "metadata_version": "2.0", "name": "cycler", "platform": "Cross platform (Linux", "run_requires": [{"requires": ["six"]}], "summary": "Composable style cycles", "version": "0.10.0"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cycler
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
"""
|
||||||
|
Cycler
|
||||||
|
======
|
||||||
|
|
||||||
|
Cycling through combinations of values, producing dictionaries.
|
||||||
|
|
||||||
|
You can add cyclers::
|
||||||
|
|
||||||
|
from cycler import cycler
|
||||||
|
cc = (cycler(color=list('rgb')) +
|
||||||
|
cycler(linestyle=['-', '--', '-.']))
|
||||||
|
for d in cc:
|
||||||
|
print(d)
|
||||||
|
|
||||||
|
Results in::
|
||||||
|
|
||||||
|
{'color': 'r', 'linestyle': '-'}
|
||||||
|
{'color': 'g', 'linestyle': '--'}
|
||||||
|
{'color': 'b', 'linestyle': '-.'}
|
||||||
|
|
||||||
|
|
||||||
|
You can multiply cyclers::
|
||||||
|
|
||||||
|
from cycler import cycler
|
||||||
|
cc = (cycler(color=list('rgb')) *
|
||||||
|
cycler(linestyle=['-', '--', '-.']))
|
||||||
|
for d in cc:
|
||||||
|
print(d)
|
||||||
|
|
||||||
|
Results in::
|
||||||
|
|
||||||
|
{'color': 'r', 'linestyle': '-'}
|
||||||
|
{'color': 'r', 'linestyle': '--'}
|
||||||
|
{'color': 'r', 'linestyle': '-.'}
|
||||||
|
{'color': 'g', 'linestyle': '-'}
|
||||||
|
{'color': 'g', 'linestyle': '--'}
|
||||||
|
{'color': 'g', 'linestyle': '-.'}
|
||||||
|
{'color': 'b', 'linestyle': '-'}
|
||||||
|
{'color': 'b', 'linestyle': '--'}
|
||||||
|
{'color': 'b', 'linestyle': '-.'}
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import (absolute_import, division, print_function,
|
||||||
|
unicode_literals)
|
||||||
|
|
||||||
|
import six
|
||||||
|
from itertools import product, cycle
|
||||||
|
from six.moves import zip, reduce
|
||||||
|
from operator import mul, add
|
||||||
|
import copy
|
||||||
|
|
||||||
|
__version__ = '0.10.0'
|
||||||
|
|
||||||
|
|
||||||
|
def _process_keys(left, right):
|
||||||
|
"""
|
||||||
|
Helper function to compose cycler keys
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
left, right : iterable of dictionaries or None
|
||||||
|
The cyclers to be composed
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
keys : set
|
||||||
|
The keys in the composition of the two cyclers
|
||||||
|
"""
|
||||||
|
l_peek = next(iter(left)) if left is not None else {}
|
||||||
|
r_peek = next(iter(right)) if right is not None else {}
|
||||||
|
l_key = set(l_peek.keys())
|
||||||
|
r_key = set(r_peek.keys())
|
||||||
|
if l_key & r_key:
|
||||||
|
raise ValueError("Can not compose overlapping cycles")
|
||||||
|
return l_key | r_key
|
||||||
|
|
||||||
|
|
||||||
|
class Cycler(object):
|
||||||
|
"""
|
||||||
|
Composable cycles
|
||||||
|
|
||||||
|
This class has compositions methods:
|
||||||
|
|
||||||
|
``+``
|
||||||
|
for 'inner' products (zip)
|
||||||
|
|
||||||
|
``+=``
|
||||||
|
in-place ``+``
|
||||||
|
|
||||||
|
``*``
|
||||||
|
for outer products (itertools.product) and integer multiplication
|
||||||
|
|
||||||
|
``*=``
|
||||||
|
in-place ``*``
|
||||||
|
|
||||||
|
and supports basic slicing via ``[]``
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
left : Cycler or None
|
||||||
|
The 'left' cycler
|
||||||
|
|
||||||
|
right : Cycler or None
|
||||||
|
The 'right' cycler
|
||||||
|
|
||||||
|
op : func or None
|
||||||
|
Function which composes the 'left' and 'right' cyclers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __call__(self):
|
||||||
|
return cycle(self)
|
||||||
|
|
||||||
|
def __init__(self, left, right=None, op=None):
|
||||||
|
"""Semi-private init
|
||||||
|
|
||||||
|
Do not use this directly, use `cycler` function instead.
|
||||||
|
"""
|
||||||
|
if isinstance(left, Cycler):
|
||||||
|
self._left = Cycler(left._left, left._right, left._op)
|
||||||
|
elif left is not None:
|
||||||
|
# Need to copy the dictionary or else that will be a residual
|
||||||
|
# mutable that could lead to strange errors
|
||||||
|
self._left = [copy.copy(v) for v in left]
|
||||||
|
else:
|
||||||
|
self._left = None
|
||||||
|
|
||||||
|
if isinstance(right, Cycler):
|
||||||
|
self._right = Cycler(right._left, right._right, right._op)
|
||||||
|
elif right is not None:
|
||||||
|
# Need to copy the dictionary or else that will be a residual
|
||||||
|
# mutable that could lead to strange errors
|
||||||
|
self._right = [copy.copy(v) for v in right]
|
||||||
|
else:
|
||||||
|
self._right = None
|
||||||
|
|
||||||
|
self._keys = _process_keys(self._left, self._right)
|
||||||
|
self._op = op
|
||||||
|
|
||||||
|
@property
|
||||||
|
def keys(self):
|
||||||
|
"""
|
||||||
|
The keys this Cycler knows about
|
||||||
|
"""
|
||||||
|
return set(self._keys)
|
||||||
|
|
||||||
|
def change_key(self, old, new):
|
||||||
|
"""
|
||||||
|
Change a key in this cycler to a new name.
|
||||||
|
Modification is performed in-place.
|
||||||
|
|
||||||
|
Does nothing if the old key is the same as the new key.
|
||||||
|
Raises a ValueError if the new key is already a key.
|
||||||
|
Raises a KeyError if the old key isn't a key.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if old == new:
|
||||||
|
return
|
||||||
|
if new in self._keys:
|
||||||
|
raise ValueError("Can't replace %s with %s, %s is already a key" %
|
||||||
|
(old, new, new))
|
||||||
|
if old not in self._keys:
|
||||||
|
raise KeyError("Can't replace %s with %s, %s is not a key" %
|
||||||
|
(old, new, old))
|
||||||
|
|
||||||
|
self._keys.remove(old)
|
||||||
|
self._keys.add(new)
|
||||||
|
|
||||||
|
if self._right is not None and old in self._right.keys:
|
||||||
|
self._right.change_key(old, new)
|
||||||
|
|
||||||
|
# self._left should always be non-None
|
||||||
|
# if self._keys is non-empty.
|
||||||
|
elif isinstance(self._left, Cycler):
|
||||||
|
self._left.change_key(old, new)
|
||||||
|
else:
|
||||||
|
# It should be completely safe at this point to
|
||||||
|
# assume that the old key can be found in each
|
||||||
|
# iteration.
|
||||||
|
self._left = [{new: entry[old]} for entry in self._left]
|
||||||
|
|
||||||
|
def _compose(self):
|
||||||
|
"""
|
||||||
|
Compose the 'left' and 'right' components of this cycle
|
||||||
|
with the proper operation (zip or product as of now)
|
||||||
|
"""
|
||||||
|
for a, b in self._op(self._left, self._right):
|
||||||
|
out = dict()
|
||||||
|
out.update(a)
|
||||||
|
out.update(b)
|
||||||
|
yield out
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _from_iter(cls, label, itr):
|
||||||
|
"""
|
||||||
|
Class method to create 'base' Cycler objects
|
||||||
|
that do not have a 'right' or 'op' and for which
|
||||||
|
the 'left' object is not another Cycler.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
label : str
|
||||||
|
The property key.
|
||||||
|
|
||||||
|
itr : iterable
|
||||||
|
Finite length iterable of the property values.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
cycler : Cycler
|
||||||
|
New 'base' `Cycler`
|
||||||
|
"""
|
||||||
|
ret = cls(None)
|
||||||
|
ret._left = list({label: v} for v in itr)
|
||||||
|
ret._keys = set([label])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
# TODO : maybe add numpy style fancy slicing
|
||||||
|
if isinstance(key, slice):
|
||||||
|
trans = self.by_key()
|
||||||
|
return reduce(add, (_cycler(k, v[key])
|
||||||
|
for k, v in six.iteritems(trans)))
|
||||||
|
else:
|
||||||
|
raise ValueError("Can only use slices with Cycler.__getitem__")
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
if self._right is None:
|
||||||
|
return iter(dict(l) for l in self._left)
|
||||||
|
|
||||||
|
return self._compose()
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
"""
|
||||||
|
Pair-wise combine two equal length cycles (zip)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
other : Cycler
|
||||||
|
The second Cycler
|
||||||
|
"""
|
||||||
|
if len(self) != len(other):
|
||||||
|
raise ValueError("Can only add equal length cycles, "
|
||||||
|
"not {0} and {1}".format(len(self), len(other)))
|
||||||
|
return Cycler(self, other, zip)
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
"""
|
||||||
|
Outer product of two cycles (`itertools.product`) or integer
|
||||||
|
multiplication.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
other : Cycler or int
|
||||||
|
The second Cycler or integer
|
||||||
|
"""
|
||||||
|
if isinstance(other, Cycler):
|
||||||
|
return Cycler(self, other, product)
|
||||||
|
elif isinstance(other, int):
|
||||||
|
trans = self.by_key()
|
||||||
|
return reduce(add, (_cycler(k, v*other)
|
||||||
|
for k, v in six.iteritems(trans)))
|
||||||
|
else:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
def __rmul__(self, other):
|
||||||
|
return self * other
|
||||||
|
|
||||||
|
def __len__(self):
|
||||||
|
op_dict = {zip: min, product: mul}
|
||||||
|
if self._right is None:
|
||||||
|
return len(self._left)
|
||||||
|
l_len = len(self._left)
|
||||||
|
r_len = len(self._right)
|
||||||
|
return op_dict[self._op](l_len, r_len)
|
||||||
|
|
||||||
|
def __iadd__(self, other):
|
||||||
|
"""
|
||||||
|
In-place pair-wise combine two equal length cycles (zip)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
other : Cycler
|
||||||
|
The second Cycler
|
||||||
|
"""
|
||||||
|
if not isinstance(other, Cycler):
|
||||||
|
raise TypeError("Cannot += with a non-Cycler object")
|
||||||
|
# True shallow copy of self is fine since this is in-place
|
||||||
|
old_self = copy.copy(self)
|
||||||
|
self._keys = _process_keys(old_self, other)
|
||||||
|
self._left = old_self
|
||||||
|
self._op = zip
|
||||||
|
self._right = Cycler(other._left, other._right, other._op)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __imul__(self, other):
|
||||||
|
"""
|
||||||
|
In-place outer product of two cycles (`itertools.product`)
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
other : Cycler
|
||||||
|
The second Cycler
|
||||||
|
"""
|
||||||
|
if not isinstance(other, Cycler):
|
||||||
|
raise TypeError("Cannot *= with a non-Cycler object")
|
||||||
|
# True shallow copy of self is fine since this is in-place
|
||||||
|
old_self = copy.copy(self)
|
||||||
|
self._keys = _process_keys(old_self, other)
|
||||||
|
self._left = old_self
|
||||||
|
self._op = product
|
||||||
|
self._right = Cycler(other._left, other._right, other._op)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
"""
|
||||||
|
Check equality
|
||||||
|
"""
|
||||||
|
if len(self) != len(other):
|
||||||
|
return False
|
||||||
|
if self.keys ^ other.keys:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return all(a == b for a, b in zip(self, other))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
op_map = {zip: '+', product: '*'}
|
||||||
|
if self._right is None:
|
||||||
|
lab = self.keys.pop()
|
||||||
|
itr = list(v[lab] for v in self)
|
||||||
|
return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr)
|
||||||
|
else:
|
||||||
|
op = op_map.get(self._op, '?')
|
||||||
|
msg = "({left!r} {op} {right!r})"
|
||||||
|
return msg.format(left=self._left, op=op, right=self._right)
|
||||||
|
|
||||||
|
def _repr_html_(self):
|
||||||
|
# an table showing the value of each key through a full cycle
|
||||||
|
output = "<table>"
|
||||||
|
sorted_keys = sorted(self.keys, key=repr)
|
||||||
|
for key in sorted_keys:
|
||||||
|
output += "<th>{key!r}</th>".format(key=key)
|
||||||
|
for d in iter(self):
|
||||||
|
output += "<tr>"
|
||||||
|
for k in sorted_keys:
|
||||||
|
output += "<td>{val!r}</td>".format(val=d[k])
|
||||||
|
output += "</tr>"
|
||||||
|
output += "</table>"
|
||||||
|
return output
|
||||||
|
|
||||||
|
def by_key(self):
|
||||||
|
"""Values by key
|
||||||
|
|
||||||
|
This returns the transposed values of the cycler. Iterating
|
||||||
|
over a `Cycler` yields dicts with a single value for each key,
|
||||||
|
this method returns a `dict` of `list` which are the values
|
||||||
|
for the given key.
|
||||||
|
|
||||||
|
The returned value can be used to create an equivalent `Cycler`
|
||||||
|
using only `+`.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
transpose : dict
|
||||||
|
dict of lists of the values for each key.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO : sort out if this is a bottle neck, if there is a better way
|
||||||
|
# and if we care.
|
||||||
|
|
||||||
|
keys = self.keys
|
||||||
|
# change this to dict comprehension when drop 2.6
|
||||||
|
out = dict((k, list()) for k in keys)
|
||||||
|
|
||||||
|
for d in self:
|
||||||
|
for k in keys:
|
||||||
|
out[k].append(d[k])
|
||||||
|
return out
|
||||||
|
|
||||||
|
# for back compatibility
|
||||||
|
_transpose = by_key
|
||||||
|
|
||||||
|
def simplify(self):
|
||||||
|
"""Simplify the Cycler
|
||||||
|
|
||||||
|
Returned as a composition using only sums (no multiplications)
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
simple : Cycler
|
||||||
|
An equivalent cycler using only summation"""
|
||||||
|
# TODO: sort out if it is worth the effort to make sure this is
|
||||||
|
# balanced. Currently it is is
|
||||||
|
# (((a + b) + c) + d) vs
|
||||||
|
# ((a + b) + (c + d))
|
||||||
|
# I would believe that there is some performance implications
|
||||||
|
|
||||||
|
trans = self.by_key()
|
||||||
|
return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans)))
|
||||||
|
|
||||||
|
def concat(self, other):
|
||||||
|
"""Concatenate this cycler and an other.
|
||||||
|
|
||||||
|
The keys must match exactly.
|
||||||
|
|
||||||
|
This returns a single Cycler which is equivalent to
|
||||||
|
`itertools.chain(self, other)`
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
>>> num = cycler('a', range(3))
|
||||||
|
>>> let = cycler('a', 'abc')
|
||||||
|
>>> num.concat(let)
|
||||||
|
cycler('a', [0, 1, 2, 'a', 'b', 'c'])
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
other : `Cycler`
|
||||||
|
The `Cycler` to concatenate to this one.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ret : `Cycler`
|
||||||
|
The concatenated `Cycler`
|
||||||
|
"""
|
||||||
|
return concat(self, other)
|
||||||
|
|
||||||
|
|
||||||
|
def concat(left, right):
|
||||||
|
"""Concatenate two cyclers.
|
||||||
|
|
||||||
|
The keys must match exactly.
|
||||||
|
|
||||||
|
This returns a single Cycler which is equivalent to
|
||||||
|
`itertools.chain(left, right)`
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
>>> num = cycler('a', range(3))
|
||||||
|
>>> let = cycler('a', 'abc')
|
||||||
|
>>> num.concat(let)
|
||||||
|
cycler('a', [0, 1, 2, 'a', 'b', 'c'])
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
left, right : `Cycler`
|
||||||
|
The two `Cycler` instances to concatenate
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ret : `Cycler`
|
||||||
|
The concatenated `Cycler`
|
||||||
|
"""
|
||||||
|
if left.keys != right.keys:
|
||||||
|
msg = '\n\t'.join(["Keys do not match:",
|
||||||
|
"Intersection: {both!r}",
|
||||||
|
"Disjoint: {just_one!r}"]).format(
|
||||||
|
both=left.keys & right.keys,
|
||||||
|
just_one=left.keys ^ right.keys)
|
||||||
|
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
_l = left.by_key()
|
||||||
|
_r = right.by_key()
|
||||||
|
return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys))
|
||||||
|
|
||||||
|
|
||||||
|
def cycler(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Create a new `Cycler` object from a single positional argument,
|
||||||
|
a pair of positional arguments, or the combination of keyword arguments.
|
||||||
|
|
||||||
|
cycler(arg)
|
||||||
|
cycler(label1=itr1[, label2=iter2[, ...]])
|
||||||
|
cycler(label, itr)
|
||||||
|
|
||||||
|
Form 1 simply copies a given `Cycler` object.
|
||||||
|
|
||||||
|
Form 2 composes a `Cycler` as an inner product of the
|
||||||
|
pairs of keyword arguments. In other words, all of the
|
||||||
|
iterables are cycled simultaneously, as if through zip().
|
||||||
|
|
||||||
|
Form 3 creates a `Cycler` from a label and an iterable.
|
||||||
|
This is useful for when the label cannot be a keyword argument
|
||||||
|
(e.g., an integer or a name that has a space in it).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
arg : Cycler
|
||||||
|
Copy constructor for Cycler (does a shallow copy of iterables).
|
||||||
|
|
||||||
|
label : name
|
||||||
|
The property key. In the 2-arg form of the function,
|
||||||
|
the label can be any hashable object. In the keyword argument
|
||||||
|
form of the function, it must be a valid python identifier.
|
||||||
|
|
||||||
|
itr : iterable
|
||||||
|
Finite length iterable of the property values.
|
||||||
|
Can be a single-property `Cycler` that would
|
||||||
|
be like a key change, but as a shallow copy.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
cycler : Cycler
|
||||||
|
New `Cycler` for the given property
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args and kwargs:
|
||||||
|
raise TypeError("cyl() can only accept positional OR keyword "
|
||||||
|
"arguments -- not both.")
|
||||||
|
|
||||||
|
if len(args) == 1:
|
||||||
|
if not isinstance(args[0], Cycler):
|
||||||
|
raise TypeError("If only one positional argument given, it must "
|
||||||
|
" be a Cycler instance.")
|
||||||
|
return Cycler(args[0])
|
||||||
|
elif len(args) == 2:
|
||||||
|
return _cycler(*args)
|
||||||
|
elif len(args) > 2:
|
||||||
|
raise TypeError("Only a single Cycler can be accepted as the lone "
|
||||||
|
"positional argument. Use keyword arguments instead.")
|
||||||
|
|
||||||
|
if kwargs:
|
||||||
|
return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs)))
|
||||||
|
|
||||||
|
raise TypeError("Must have at least a positional OR keyword arguments")
|
||||||
|
|
||||||
|
|
||||||
|
def _cycler(label, itr):
|
||||||
|
"""
|
||||||
|
Create a new `Cycler` object from a property name and
|
||||||
|
iterable of values.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
label : hashable
|
||||||
|
The property key.
|
||||||
|
|
||||||
|
itr : iterable
|
||||||
|
Finite length iterable of the property values.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
cycler : Cycler
|
||||||
|
New `Cycler` for the given property
|
||||||
|
"""
|
||||||
|
if isinstance(itr, Cycler):
|
||||||
|
keys = itr.keys
|
||||||
|
if len(keys) != 1:
|
||||||
|
msg = "Can not create Cycler from a multi-property Cycler"
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
lab = keys.pop()
|
||||||
|
# Doesn't need to be a new list because
|
||||||
|
# _from_iter() will be creating that new list anyway.
|
||||||
|
itr = (v[lab] for v in itr)
|
||||||
|
|
||||||
|
return Cycler._from_iter(label, itr)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
try:
|
||||||
|
from ._version import version as __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = 'unknown'
|
||||||
|
|
||||||
|
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||||
|
'utils', 'zoneinfo']
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
Common code used in multiple modules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class weekday(object):
|
||||||
|
__slots__ = ["weekday", "n"]
|
||||||
|
|
||||||
|
def __init__(self, weekday, n=None):
|
||||||
|
self.weekday = weekday
|
||||||
|
self.n = n
|
||||||
|
|
||||||
|
def __call__(self, n):
|
||||||
|
if n == self.n:
|
||||||
|
return self
|
||||||
|
else:
|
||||||
|
return self.__class__(self.weekday, n)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
try:
|
||||||
|
if self.weekday != other.weekday or self.n != other.n:
|
||||||
|
return False
|
||||||
|
except AttributeError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.weekday,
|
||||||
|
self.n,
|
||||||
|
))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||||
|
if not self.n:
|
||||||
|
return s
|
||||||
|
else:
|
||||||
|
return "%s(%+d)" % (s, self.n)
|
||||||
|
|
||||||
|
# vim:ts=4:sw=4:et
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# coding: utf-8
|
||||||
|
# file generated by setuptools_scm
|
||||||
|
# don't change, don't track in version control
|
||||||
|
version = '2.7.5'
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers a generic easter computing method for any given year, using
|
||||||
|
Western, Orthodox or Julian algorithms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||||
|
|
||||||
|
EASTER_JULIAN = 1
|
||||||
|
EASTER_ORTHODOX = 2
|
||||||
|
EASTER_WESTERN = 3
|
||||||
|
|
||||||
|
|
||||||
|
def easter(year, method=EASTER_WESTERN):
|
||||||
|
"""
|
||||||
|
This method was ported from the work done by GM Arts,
|
||||||
|
on top of the algorithm by Claus Tondering, which was
|
||||||
|
based in part on the algorithm of Ouding (1940), as
|
||||||
|
quoted in "Explanatory Supplement to the Astronomical
|
||||||
|
Almanac", P. Kenneth Seidelmann, editor.
|
||||||
|
|
||||||
|
This algorithm implements three different easter
|
||||||
|
calculation methods:
|
||||||
|
|
||||||
|
1 - Original calculation in Julian calendar, valid in
|
||||||
|
dates after 326 AD
|
||||||
|
2 - Original method, with date converted to Gregorian
|
||||||
|
calendar, valid in years 1583 to 4099
|
||||||
|
3 - Revised method, in Gregorian calendar, valid in
|
||||||
|
years 1583 to 4099 as well
|
||||||
|
|
||||||
|
These methods are represented by the constants:
|
||||||
|
|
||||||
|
* ``EASTER_JULIAN = 1``
|
||||||
|
* ``EASTER_ORTHODOX = 2``
|
||||||
|
* ``EASTER_WESTERN = 3``
|
||||||
|
|
||||||
|
The default method is method 3.
|
||||||
|
|
||||||
|
More about the algorithm may be found at:
|
||||||
|
|
||||||
|
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (1 <= method <= 3):
|
||||||
|
raise ValueError("invalid method")
|
||||||
|
|
||||||
|
# g - Golden year - 1
|
||||||
|
# c - Century
|
||||||
|
# h - (23 - Epact) mod 30
|
||||||
|
# i - Number of days from March 21 to Paschal Full Moon
|
||||||
|
# j - Weekday for PFM (0=Sunday, etc)
|
||||||
|
# p - Number of days from March 21 to Sunday on or before PFM
|
||||||
|
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||||
|
# e - Extra days to add for method 2 (converting Julian
|
||||||
|
# date to Gregorian date)
|
||||||
|
|
||||||
|
y = year
|
||||||
|
g = y % 19
|
||||||
|
e = 0
|
||||||
|
if method < 3:
|
||||||
|
# Old method
|
||||||
|
i = (19*g + 15) % 30
|
||||||
|
j = (y + y//4 + i) % 7
|
||||||
|
if method == 2:
|
||||||
|
# Extra dates to convert Julian to Gregorian date
|
||||||
|
e = 10
|
||||||
|
if y > 1600:
|
||||||
|
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||||
|
else:
|
||||||
|
# New method
|
||||||
|
c = y//100
|
||||||
|
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||||
|
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||||
|
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||||
|
|
||||||
|
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||||
|
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||||
|
p = i - j + e
|
||||||
|
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||||
|
m = 3 + (p + 26)//30
|
||||||
|
return datetime.date(int(y), int(m), int(d))
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from ._parser import parse, parser, parserinfo
|
||||||
|
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||||
|
from ._parser import UnknownTimezoneWarning
|
||||||
|
|
||||||
|
from ._parser import __doc__
|
||||||
|
|
||||||
|
from .isoparser import isoparser, isoparse
|
||||||
|
|
||||||
|
__all__ = ['parse', 'parser', 'parserinfo',
|
||||||
|
'isoparse', 'isoparser',
|
||||||
|
'UnknownTimezoneWarning']
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Deprecate portions of the private interface so that downstream code that
|
||||||
|
# is improperly relying on it is given *some* notice.
|
||||||
|
|
||||||
|
|
||||||
|
def __deprecated_private_func(f):
|
||||||
|
from functools import wraps
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private function and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=f.__name__)
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
|
def deprecated_func(*args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return deprecated_func
|
||||||
|
|
||||||
|
def __deprecate_private_class(c):
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
msg = ('{name} is a private class and may break without warning, '
|
||||||
|
'it will be moved and or renamed in future versions.')
|
||||||
|
msg = msg.format(name=c.__name__)
|
||||||
|
|
||||||
|
class private_class(c):
|
||||||
|
__doc__ = c.__doc__
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
warnings.warn(msg, DeprecationWarning)
|
||||||
|
super(private_class, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
private_class.__name__ = c.__name__
|
||||||
|
|
||||||
|
return private_class
|
||||||
|
|
||||||
|
|
||||||
|
from ._parser import _timelex, _resultbase
|
||||||
|
from ._parser import _tzparser, _parsetz
|
||||||
|
|
||||||
|
_timelex = __deprecate_private_class(_timelex)
|
||||||
|
_tzparser = __deprecate_private_class(_tzparser)
|
||||||
|
_resultbase = __deprecate_private_class(_resultbase)
|
||||||
|
_parsetz = __deprecated_private_func(_parsetz)
|
||||||
@@ -0,0 +1,406 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers a parser for ISO-8601 strings
|
||||||
|
|
||||||
|
It is intended to support all valid date, time and datetime formats per the
|
||||||
|
ISO-8601 specification.
|
||||||
|
|
||||||
|
..versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timedelta, time, date
|
||||||
|
import calendar
|
||||||
|
from dateutil import tz
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
import re
|
||||||
|
import six
|
||||||
|
|
||||||
|
__all__ = ["isoparse", "isoparser"]
|
||||||
|
|
||||||
|
|
||||||
|
def _takes_ascii(f):
|
||||||
|
@wraps(f)
|
||||||
|
def func(self, str_in, *args, **kwargs):
|
||||||
|
# If it's a stream, read the whole thing
|
||||||
|
str_in = getattr(str_in, 'read', lambda: str_in)()
|
||||||
|
|
||||||
|
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
||||||
|
if isinstance(str_in, six.text_type):
|
||||||
|
# ASCII is the same in UTF-8
|
||||||
|
try:
|
||||||
|
str_in = str_in.encode('ascii')
|
||||||
|
except UnicodeEncodeError as e:
|
||||||
|
msg = 'ISO-8601 strings should contain only ASCII characters'
|
||||||
|
six.raise_from(ValueError(msg), e)
|
||||||
|
|
||||||
|
return f(self, str_in, *args, **kwargs)
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class isoparser(object):
|
||||||
|
def __init__(self, sep=None):
|
||||||
|
"""
|
||||||
|
:param sep:
|
||||||
|
A single character that separates date and time portions. If
|
||||||
|
``None``, the parser will accept any single character.
|
||||||
|
For strict ISO-8601 adherence, pass ``'T'``.
|
||||||
|
"""
|
||||||
|
if sep is not None:
|
||||||
|
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
||||||
|
raise ValueError('Separator must be a single, non-numeric ' +
|
||||||
|
'ASCII character')
|
||||||
|
|
||||||
|
sep = sep.encode('ascii')
|
||||||
|
|
||||||
|
self._sep = sep
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def isoparse(self, dt_str):
|
||||||
|
"""
|
||||||
|
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
||||||
|
|
||||||
|
An ISO-8601 datetime string consists of a date portion, followed
|
||||||
|
optionally by a time portion - the date and time portions are separated
|
||||||
|
by a single character separator, which is ``T`` in the official
|
||||||
|
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
||||||
|
combined with a time portion.
|
||||||
|
|
||||||
|
Supported date formats are:
|
||||||
|
|
||||||
|
Common:
|
||||||
|
|
||||||
|
- ``YYYY``
|
||||||
|
- ``YYYY-MM`` or ``YYYYMM``
|
||||||
|
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||||
|
|
||||||
|
Uncommon:
|
||||||
|
|
||||||
|
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
||||||
|
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
||||||
|
|
||||||
|
The ISO week and day numbering follows the same logic as
|
||||||
|
:func:`datetime.date.isocalendar`.
|
||||||
|
|
||||||
|
Supported time formats are:
|
||||||
|
|
||||||
|
- ``hh``
|
||||||
|
- ``hh:mm`` or ``hhmm``
|
||||||
|
- ``hh:mm:ss`` or ``hhmmss``
|
||||||
|
- ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits)
|
||||||
|
|
||||||
|
Midnight is a special case for `hh`, as the standard supports both
|
||||||
|
00:00 and 24:00 as a representation.
|
||||||
|
|
||||||
|
.. caution::
|
||||||
|
|
||||||
|
Support for fractional components other than seconds is part of the
|
||||||
|
ISO-8601 standard, but is not currently implemented in this parser.
|
||||||
|
|
||||||
|
Supported time zone offset formats are:
|
||||||
|
|
||||||
|
- `Z` (UTC)
|
||||||
|
- `±HH:MM`
|
||||||
|
- `±HHMM`
|
||||||
|
- `±HH`
|
||||||
|
|
||||||
|
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
||||||
|
with the exception of UTC, which will be represented as
|
||||||
|
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
||||||
|
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
||||||
|
|
||||||
|
:param dt_str:
|
||||||
|
A string or stream containing only an ISO-8601 datetime string
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.datetime` representing the string.
|
||||||
|
Unspecified components default to their lowest value.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
As of version 2.7.0, the strictness of the parser should not be
|
||||||
|
considered a stable part of the contract. Any valid ISO-8601 string
|
||||||
|
that parses correctly with the default settings will continue to
|
||||||
|
parse correctly in future versions, but invalid strings that
|
||||||
|
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
||||||
|
guaranteed to continue failing in future versions if they encode
|
||||||
|
a valid date.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
components, pos = self._parse_isodate(dt_str)
|
||||||
|
|
||||||
|
if len(dt_str) > pos:
|
||||||
|
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
||||||
|
components += self._parse_isotime(dt_str[pos + 1:])
|
||||||
|
else:
|
||||||
|
raise ValueError('String contains unknown ISO components')
|
||||||
|
|
||||||
|
return datetime(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_isodate(self, datestr):
|
||||||
|
"""
|
||||||
|
Parse the date portion of an ISO string.
|
||||||
|
|
||||||
|
:param datestr:
|
||||||
|
The string portion of an ISO string, without a separator
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.date` object
|
||||||
|
"""
|
||||||
|
components, pos = self._parse_isodate(datestr)
|
||||||
|
if pos < len(datestr):
|
||||||
|
raise ValueError('String contains unknown ISO ' +
|
||||||
|
'components: {}'.format(datestr))
|
||||||
|
return date(*components)
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_isotime(self, timestr):
|
||||||
|
"""
|
||||||
|
Parse the time portion of an ISO string.
|
||||||
|
|
||||||
|
:param timestr:
|
||||||
|
The time portion of an ISO string, without a separator
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.time` object
|
||||||
|
"""
|
||||||
|
return time(*self._parse_isotime(timestr))
|
||||||
|
|
||||||
|
@_takes_ascii
|
||||||
|
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
"""
|
||||||
|
Parse a valid ISO time zone string.
|
||||||
|
|
||||||
|
See :func:`isoparser.isoparse` for details on supported formats.
|
||||||
|
|
||||||
|
:param tzstr:
|
||||||
|
A string representing an ISO time zone offset
|
||||||
|
|
||||||
|
:param zero_as_utc:
|
||||||
|
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
||||||
|
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
||||||
|
specified) offsets equivalent to UTC.
|
||||||
|
"""
|
||||||
|
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
_MICROSECOND_END_REGEX = re.compile(b'[-+Z]+')
|
||||||
|
_DATE_SEP = b'-'
|
||||||
|
_TIME_SEP = b':'
|
||||||
|
_MICRO_SEP = b'.'
|
||||||
|
|
||||||
|
def _parse_isodate(self, dt_str):
|
||||||
|
try:
|
||||||
|
return self._parse_isodate_common(dt_str)
|
||||||
|
except ValueError:
|
||||||
|
return self._parse_isodate_uncommon(dt_str)
|
||||||
|
|
||||||
|
def _parse_isodate_common(self, dt_str):
|
||||||
|
len_str = len(dt_str)
|
||||||
|
components = [1, 1, 1]
|
||||||
|
|
||||||
|
if len_str < 4:
|
||||||
|
raise ValueError('ISO string too short')
|
||||||
|
|
||||||
|
# Year
|
||||||
|
components[0] = int(dt_str[0:4])
|
||||||
|
pos = 4
|
||||||
|
if pos >= len_str:
|
||||||
|
return components, pos
|
||||||
|
|
||||||
|
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
||||||
|
if has_sep:
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Month
|
||||||
|
if len_str - pos < 2:
|
||||||
|
raise ValueError('Invalid common month')
|
||||||
|
|
||||||
|
components[1] = int(dt_str[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
if pos >= len_str:
|
||||||
|
if has_sep:
|
||||||
|
return components, pos
|
||||||
|
else:
|
||||||
|
raise ValueError('Invalid ISO format')
|
||||||
|
|
||||||
|
if has_sep:
|
||||||
|
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
||||||
|
raise ValueError('Invalid separator in ISO string')
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
# Day
|
||||||
|
if len_str - pos < 2:
|
||||||
|
raise ValueError('Invalid common day')
|
||||||
|
components[2] = int(dt_str[pos:pos + 2])
|
||||||
|
return components, pos + 2
|
||||||
|
|
||||||
|
def _parse_isodate_uncommon(self, dt_str):
|
||||||
|
if len(dt_str) < 4:
|
||||||
|
raise ValueError('ISO string too short')
|
||||||
|
|
||||||
|
# All ISO formats start with the year
|
||||||
|
year = int(dt_str[0:4])
|
||||||
|
|
||||||
|
has_sep = dt_str[4:5] == self._DATE_SEP
|
||||||
|
|
||||||
|
pos = 4 + has_sep # Skip '-' if it's there
|
||||||
|
if dt_str[pos:pos + 1] == b'W':
|
||||||
|
# YYYY-?Www-?D?
|
||||||
|
pos += 1
|
||||||
|
weekno = int(dt_str[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
|
||||||
|
dayno = 1
|
||||||
|
if len(dt_str) > pos:
|
||||||
|
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
||||||
|
raise ValueError('Inconsistent use of dash separator')
|
||||||
|
|
||||||
|
pos += has_sep
|
||||||
|
|
||||||
|
dayno = int(dt_str[pos:pos + 1])
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
base_date = self._calculate_weekdate(year, weekno, dayno)
|
||||||
|
else:
|
||||||
|
# YYYYDDD or YYYY-DDD
|
||||||
|
if len(dt_str) - pos < 3:
|
||||||
|
raise ValueError('Invalid ordinal day')
|
||||||
|
|
||||||
|
ordinal_day = int(dt_str[pos:pos + 3])
|
||||||
|
pos += 3
|
||||||
|
|
||||||
|
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
||||||
|
raise ValueError('Invalid ordinal day' +
|
||||||
|
' {} for year {}'.format(ordinal_day, year))
|
||||||
|
|
||||||
|
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
||||||
|
|
||||||
|
components = [base_date.year, base_date.month, base_date.day]
|
||||||
|
return components, pos
|
||||||
|
|
||||||
|
def _calculate_weekdate(self, year, week, day):
|
||||||
|
"""
|
||||||
|
Calculate the day of corresponding to the ISO year-week-day calendar.
|
||||||
|
|
||||||
|
This function is effectively the inverse of
|
||||||
|
:func:`datetime.date.isocalendar`.
|
||||||
|
|
||||||
|
:param year:
|
||||||
|
The year in the ISO calendar
|
||||||
|
|
||||||
|
:param week:
|
||||||
|
The week in the ISO calendar - range is [1, 53]
|
||||||
|
|
||||||
|
:param day:
|
||||||
|
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`datetime.date`
|
||||||
|
"""
|
||||||
|
if not 0 < week < 54:
|
||||||
|
raise ValueError('Invalid week: {}'.format(week))
|
||||||
|
|
||||||
|
if not 0 < day < 8: # Range is 1-7
|
||||||
|
raise ValueError('Invalid weekday: {}'.format(day))
|
||||||
|
|
||||||
|
# Get week 1 for the specific year:
|
||||||
|
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
||||||
|
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
||||||
|
|
||||||
|
# Now add the specific number of weeks and days to get what we want
|
||||||
|
week_offset = (week - 1) * 7 + (day - 1)
|
||||||
|
return week_1 + timedelta(days=week_offset)
|
||||||
|
|
||||||
|
def _parse_isotime(self, timestr):
|
||||||
|
len_str = len(timestr)
|
||||||
|
components = [0, 0, 0, 0, None]
|
||||||
|
pos = 0
|
||||||
|
comp = -1
|
||||||
|
|
||||||
|
if len(timestr) < 2:
|
||||||
|
raise ValueError('ISO time too short')
|
||||||
|
|
||||||
|
has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP
|
||||||
|
|
||||||
|
while pos < len_str and comp < 5:
|
||||||
|
comp += 1
|
||||||
|
|
||||||
|
if timestr[pos:pos + 1] in b'-+Z':
|
||||||
|
# Detect time zone boundary
|
||||||
|
components[-1] = self._parse_tzstr(timestr[pos:])
|
||||||
|
pos = len_str
|
||||||
|
break
|
||||||
|
|
||||||
|
if comp < 3:
|
||||||
|
# Hour, minute, second
|
||||||
|
components[comp] = int(timestr[pos:pos + 2])
|
||||||
|
pos += 2
|
||||||
|
if (has_sep and pos < len_str and
|
||||||
|
timestr[pos:pos + 1] == self._TIME_SEP):
|
||||||
|
pos += 1
|
||||||
|
|
||||||
|
if comp == 3:
|
||||||
|
# Microsecond
|
||||||
|
if timestr[pos:pos + 1] != self._MICRO_SEP:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pos += 1
|
||||||
|
us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6],
|
||||||
|
1)[0]
|
||||||
|
|
||||||
|
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||||
|
pos += len(us_str)
|
||||||
|
|
||||||
|
if pos < len_str:
|
||||||
|
raise ValueError('Unused components in ISO string')
|
||||||
|
|
||||||
|
if components[0] == 24:
|
||||||
|
# Standard supports 00:00 and 24:00 as representations of midnight
|
||||||
|
if any(component != 0 for component in components[1:4]):
|
||||||
|
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
||||||
|
components[0] = 0
|
||||||
|
|
||||||
|
return components
|
||||||
|
|
||||||
|
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||||
|
if tzstr == b'Z':
|
||||||
|
return tz.tzutc()
|
||||||
|
|
||||||
|
if len(tzstr) not in {3, 5, 6}:
|
||||||
|
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
||||||
|
|
||||||
|
if tzstr[0:1] == b'-':
|
||||||
|
mult = -1
|
||||||
|
elif tzstr[0:1] == b'+':
|
||||||
|
mult = 1
|
||||||
|
else:
|
||||||
|
raise ValueError('Time zone offset requires sign')
|
||||||
|
|
||||||
|
hours = int(tzstr[1:3])
|
||||||
|
if len(tzstr) == 3:
|
||||||
|
minutes = 0
|
||||||
|
else:
|
||||||
|
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
||||||
|
|
||||||
|
if zero_as_utc and hours == 0 and minutes == 0:
|
||||||
|
return tz.tzutc()
|
||||||
|
else:
|
||||||
|
if minutes > 59:
|
||||||
|
raise ValueError('Invalid minutes in time zone offset')
|
||||||
|
|
||||||
|
if hours > 23:
|
||||||
|
raise ValueError('Invalid hours in time zone offset')
|
||||||
|
|
||||||
|
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ISOPARSER = isoparser()
|
||||||
|
isoparse = DEFAULT_ISOPARSER.isoparse
|
||||||
@@ -0,0 +1,590 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
|
import calendar
|
||||||
|
|
||||||
|
import operator
|
||||||
|
from math import copysign
|
||||||
|
|
||||||
|
from six import integer_types
|
||||||
|
from warnings import warn
|
||||||
|
|
||||||
|
from ._common import weekday
|
||||||
|
|
||||||
|
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||||
|
|
||||||
|
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||||
|
|
||||||
|
|
||||||
|
class relativedelta(object):
|
||||||
|
"""
|
||||||
|
The relativedelta type is based on the specification of the excellent
|
||||||
|
work done by M.-A. Lemburg in his
|
||||||
|
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
||||||
|
However, notice that this type does *NOT* implement the same algorithm as
|
||||||
|
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||||
|
|
||||||
|
There are two different ways to build a relativedelta instance. The
|
||||||
|
first one is passing it two date/datetime classes::
|
||||||
|
|
||||||
|
relativedelta(datetime1, datetime2)
|
||||||
|
|
||||||
|
The second one is passing it any number of the following keyword arguments::
|
||||||
|
|
||||||
|
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||||
|
|
||||||
|
year, month, day, hour, minute, second, microsecond:
|
||||||
|
Absolute information (argument is singular); adding or subtracting a
|
||||||
|
relativedelta with absolute information does not perform an arithmetic
|
||||||
|
operation, but rather REPLACES the corresponding value in the
|
||||||
|
original datetime with the value(s) in relativedelta.
|
||||||
|
|
||||||
|
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||||
|
Relative information, may be negative (argument is plural); adding
|
||||||
|
or subtracting a relativedelta with relative information performs
|
||||||
|
the corresponding aritmetic operation on the original datetime value
|
||||||
|
with the information in the relativedelta.
|
||||||
|
|
||||||
|
weekday:
|
||||||
|
One of the weekday instances (MO, TU, etc). These
|
||||||
|
instances may receive a parameter N, specifying the Nth
|
||||||
|
weekday, which could be positive or negative (like MO(+1)
|
||||||
|
or MO(-2). Not specifying it is the same as specifying
|
||||||
|
+1. You can also use an integer, where 0=MO. Notice that
|
||||||
|
if the calculated date is already Monday, for example,
|
||||||
|
using MO(1) or MO(-1) won't change the day.
|
||||||
|
|
||||||
|
leapdays:
|
||||||
|
Will add given days to the date found, if year is a leap
|
||||||
|
year, and the date found is post 28 of february.
|
||||||
|
|
||||||
|
yearday, nlyearday:
|
||||||
|
Set the yearday or the non-leap year day (jump leap days).
|
||||||
|
These are converted to day/month/leapdays information.
|
||||||
|
|
||||||
|
There are relative and absolute forms of the keyword
|
||||||
|
arguments. The plural is relative, and the singular is
|
||||||
|
absolute. For each argument in the order below, the absolute form
|
||||||
|
is applied first (by setting each attribute to that value) and
|
||||||
|
then the relative form (by adding the value to the attribute).
|
||||||
|
|
||||||
|
The order of attributes considered when this relativedelta is
|
||||||
|
added to a datetime is:
|
||||||
|
|
||||||
|
1. Year
|
||||||
|
2. Month
|
||||||
|
3. Day
|
||||||
|
4. Hours
|
||||||
|
5. Minutes
|
||||||
|
6. Seconds
|
||||||
|
7. Microseconds
|
||||||
|
|
||||||
|
Finally, weekday is applied, using the rule described above.
|
||||||
|
|
||||||
|
For example
|
||||||
|
|
||||||
|
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
||||||
|
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
||||||
|
datetime(2018, 4, 2, 14, 37, 0)
|
||||||
|
|
||||||
|
First, the day is set to 1 (the first of the month), then 25 hours
|
||||||
|
are added, to get to the 2nd day and 14th hour, finally the
|
||||||
|
weekday is applied, but since the 2nd is already a Monday there is
|
||||||
|
no effect.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, dt1=None, dt2=None,
|
||||||
|
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||||
|
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||||
|
year=None, month=None, day=None, weekday=None,
|
||||||
|
yearday=None, nlyearday=None,
|
||||||
|
hour=None, minute=None, second=None, microsecond=None):
|
||||||
|
|
||||||
|
if dt1 and dt2:
|
||||||
|
# datetime is a subclass of date. So both must be date
|
||||||
|
if not (isinstance(dt1, datetime.date) and
|
||||||
|
isinstance(dt2, datetime.date)):
|
||||||
|
raise TypeError("relativedelta only diffs datetime/date")
|
||||||
|
|
||||||
|
# We allow two dates, or two datetimes, so we coerce them to be
|
||||||
|
# of the same type
|
||||||
|
if (isinstance(dt1, datetime.datetime) !=
|
||||||
|
isinstance(dt2, datetime.datetime)):
|
||||||
|
if not isinstance(dt1, datetime.datetime):
|
||||||
|
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||||
|
elif not isinstance(dt2, datetime.datetime):
|
||||||
|
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||||
|
|
||||||
|
self.years = 0
|
||||||
|
self.months = 0
|
||||||
|
self.days = 0
|
||||||
|
self.leapdays = 0
|
||||||
|
self.hours = 0
|
||||||
|
self.minutes = 0
|
||||||
|
self.seconds = 0
|
||||||
|
self.microseconds = 0
|
||||||
|
self.year = None
|
||||||
|
self.month = None
|
||||||
|
self.day = None
|
||||||
|
self.weekday = None
|
||||||
|
self.hour = None
|
||||||
|
self.minute = None
|
||||||
|
self.second = None
|
||||||
|
self.microsecond = None
|
||||||
|
self._has_time = 0
|
||||||
|
|
||||||
|
# Get year / month delta between the two
|
||||||
|
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
||||||
|
self._set_months(months)
|
||||||
|
|
||||||
|
# Remove the year/month delta so the timedelta is just well-defined
|
||||||
|
# time units (seconds, days and microseconds)
|
||||||
|
dtm = self.__radd__(dt2)
|
||||||
|
|
||||||
|
# If we've overshot our target, make an adjustment
|
||||||
|
if dt1 < dt2:
|
||||||
|
compare = operator.gt
|
||||||
|
increment = 1
|
||||||
|
else:
|
||||||
|
compare = operator.lt
|
||||||
|
increment = -1
|
||||||
|
|
||||||
|
while compare(dt1, dtm):
|
||||||
|
months += increment
|
||||||
|
self._set_months(months)
|
||||||
|
dtm = self.__radd__(dt2)
|
||||||
|
|
||||||
|
# Get the timedelta between the "months-adjusted" date and dt1
|
||||||
|
delta = dt1 - dtm
|
||||||
|
self.seconds = delta.seconds + delta.days * 86400
|
||||||
|
self.microseconds = delta.microseconds
|
||||||
|
else:
|
||||||
|
# Check for non-integer values in integer-only quantities
|
||||||
|
if any(x is not None and x != int(x) for x in (years, months)):
|
||||||
|
raise ValueError("Non-integer years and months are "
|
||||||
|
"ambiguous and not currently supported.")
|
||||||
|
|
||||||
|
# Relative information
|
||||||
|
self.years = int(years)
|
||||||
|
self.months = int(months)
|
||||||
|
self.days = days + weeks * 7
|
||||||
|
self.leapdays = leapdays
|
||||||
|
self.hours = hours
|
||||||
|
self.minutes = minutes
|
||||||
|
self.seconds = seconds
|
||||||
|
self.microseconds = microseconds
|
||||||
|
|
||||||
|
# Absolute information
|
||||||
|
self.year = year
|
||||||
|
self.month = month
|
||||||
|
self.day = day
|
||||||
|
self.hour = hour
|
||||||
|
self.minute = minute
|
||||||
|
self.second = second
|
||||||
|
self.microsecond = microsecond
|
||||||
|
|
||||||
|
if any(x is not None and int(x) != x
|
||||||
|
for x in (year, month, day, hour,
|
||||||
|
minute, second, microsecond)):
|
||||||
|
# For now we'll deprecate floats - later it'll be an error.
|
||||||
|
warn("Non-integer value passed as absolute information. " +
|
||||||
|
"This is not a well-defined condition and will raise " +
|
||||||
|
"errors in future versions.", DeprecationWarning)
|
||||||
|
|
||||||
|
if isinstance(weekday, integer_types):
|
||||||
|
self.weekday = weekdays[weekday]
|
||||||
|
else:
|
||||||
|
self.weekday = weekday
|
||||||
|
|
||||||
|
yday = 0
|
||||||
|
if nlyearday:
|
||||||
|
yday = nlyearday
|
||||||
|
elif yearday:
|
||||||
|
yday = yearday
|
||||||
|
if yearday > 59:
|
||||||
|
self.leapdays = -1
|
||||||
|
if yday:
|
||||||
|
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||||
|
243, 273, 304, 334, 366]
|
||||||
|
for idx, ydays in enumerate(ydayidx):
|
||||||
|
if yday <= ydays:
|
||||||
|
self.month = idx+1
|
||||||
|
if idx == 0:
|
||||||
|
self.day = yday
|
||||||
|
else:
|
||||||
|
self.day = yday-ydayidx[idx-1]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid year day (%d)" % yday)
|
||||||
|
|
||||||
|
self._fix()
|
||||||
|
|
||||||
|
def _fix(self):
|
||||||
|
if abs(self.microseconds) > 999999:
|
||||||
|
s = _sign(self.microseconds)
|
||||||
|
div, mod = divmod(self.microseconds * s, 1000000)
|
||||||
|
self.microseconds = mod * s
|
||||||
|
self.seconds += div * s
|
||||||
|
if abs(self.seconds) > 59:
|
||||||
|
s = _sign(self.seconds)
|
||||||
|
div, mod = divmod(self.seconds * s, 60)
|
||||||
|
self.seconds = mod * s
|
||||||
|
self.minutes += div * s
|
||||||
|
if abs(self.minutes) > 59:
|
||||||
|
s = _sign(self.minutes)
|
||||||
|
div, mod = divmod(self.minutes * s, 60)
|
||||||
|
self.minutes = mod * s
|
||||||
|
self.hours += div * s
|
||||||
|
if abs(self.hours) > 23:
|
||||||
|
s = _sign(self.hours)
|
||||||
|
div, mod = divmod(self.hours * s, 24)
|
||||||
|
self.hours = mod * s
|
||||||
|
self.days += div * s
|
||||||
|
if abs(self.months) > 11:
|
||||||
|
s = _sign(self.months)
|
||||||
|
div, mod = divmod(self.months * s, 12)
|
||||||
|
self.months = mod * s
|
||||||
|
self.years += div * s
|
||||||
|
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||||
|
or self.hour is not None or self.minute is not None or
|
||||||
|
self.second is not None or self.microsecond is not None):
|
||||||
|
self._has_time = 1
|
||||||
|
else:
|
||||||
|
self._has_time = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def weeks(self):
|
||||||
|
return int(self.days / 7.0)
|
||||||
|
|
||||||
|
@weeks.setter
|
||||||
|
def weeks(self, value):
|
||||||
|
self.days = self.days - (self.weeks * 7) + value * 7
|
||||||
|
|
||||||
|
def _set_months(self, months):
|
||||||
|
self.months = months
|
||||||
|
if abs(self.months) > 11:
|
||||||
|
s = _sign(self.months)
|
||||||
|
div, mod = divmod(self.months * s, 12)
|
||||||
|
self.months = mod * s
|
||||||
|
self.years = div * s
|
||||||
|
else:
|
||||||
|
self.years = 0
|
||||||
|
|
||||||
|
def normalized(self):
|
||||||
|
"""
|
||||||
|
Return a version of this object represented entirely using integer
|
||||||
|
values for the relative attributes.
|
||||||
|
|
||||||
|
>>> relativedelta(days=1.5, hours=2).normalized()
|
||||||
|
relativedelta(days=1, hours=14)
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
||||||
|
"""
|
||||||
|
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
||||||
|
days = int(self.days)
|
||||||
|
|
||||||
|
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
||||||
|
hours = int(hours_f)
|
||||||
|
|
||||||
|
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
||||||
|
minutes = int(minutes_f)
|
||||||
|
|
||||||
|
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
||||||
|
seconds = int(seconds_f)
|
||||||
|
|
||||||
|
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
||||||
|
|
||||||
|
# Constructor carries overflow back up with call to _fix()
|
||||||
|
return self.__class__(years=self.years, months=self.months,
|
||||||
|
days=days, hours=hours, minutes=minutes,
|
||||||
|
seconds=seconds, microseconds=microseconds,
|
||||||
|
leapdays=self.leapdays, year=self.year,
|
||||||
|
month=self.month, day=self.day,
|
||||||
|
weekday=self.weekday, hour=self.hour,
|
||||||
|
minute=self.minute, second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __add__(self, other):
|
||||||
|
if isinstance(other, relativedelta):
|
||||||
|
return self.__class__(years=other.years + self.years,
|
||||||
|
months=other.months + self.months,
|
||||||
|
days=other.days + self.days,
|
||||||
|
hours=other.hours + self.hours,
|
||||||
|
minutes=other.minutes + self.minutes,
|
||||||
|
seconds=other.seconds + self.seconds,
|
||||||
|
microseconds=(other.microseconds +
|
||||||
|
self.microseconds),
|
||||||
|
leapdays=other.leapdays or self.leapdays,
|
||||||
|
year=(other.year if other.year is not None
|
||||||
|
else self.year),
|
||||||
|
month=(other.month if other.month is not None
|
||||||
|
else self.month),
|
||||||
|
day=(other.day if other.day is not None
|
||||||
|
else self.day),
|
||||||
|
weekday=(other.weekday if other.weekday is not None
|
||||||
|
else self.weekday),
|
||||||
|
hour=(other.hour if other.hour is not None
|
||||||
|
else self.hour),
|
||||||
|
minute=(other.minute if other.minute is not None
|
||||||
|
else self.minute),
|
||||||
|
second=(other.second if other.second is not None
|
||||||
|
else self.second),
|
||||||
|
microsecond=(other.microsecond if other.microsecond
|
||||||
|
is not None else
|
||||||
|
self.microsecond))
|
||||||
|
if isinstance(other, datetime.timedelta):
|
||||||
|
return self.__class__(years=self.years,
|
||||||
|
months=self.months,
|
||||||
|
days=self.days + other.days,
|
||||||
|
hours=self.hours,
|
||||||
|
minutes=self.minutes,
|
||||||
|
seconds=self.seconds + other.seconds,
|
||||||
|
microseconds=self.microseconds + other.microseconds,
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
if not isinstance(other, datetime.date):
|
||||||
|
return NotImplemented
|
||||||
|
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||||
|
other = datetime.datetime.fromordinal(other.toordinal())
|
||||||
|
year = (self.year or other.year)+self.years
|
||||||
|
month = self.month or other.month
|
||||||
|
if self.months:
|
||||||
|
assert 1 <= abs(self.months) <= 12
|
||||||
|
month += self.months
|
||||||
|
if month > 12:
|
||||||
|
year += 1
|
||||||
|
month -= 12
|
||||||
|
elif month < 1:
|
||||||
|
year -= 1
|
||||||
|
month += 12
|
||||||
|
day = min(calendar.monthrange(year, month)[1],
|
||||||
|
self.day or other.day)
|
||||||
|
repl = {"year": year, "month": month, "day": day}
|
||||||
|
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value is not None:
|
||||||
|
repl[attr] = value
|
||||||
|
days = self.days
|
||||||
|
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||||
|
days += self.leapdays
|
||||||
|
ret = (other.replace(**repl)
|
||||||
|
+ datetime.timedelta(days=days,
|
||||||
|
hours=self.hours,
|
||||||
|
minutes=self.minutes,
|
||||||
|
seconds=self.seconds,
|
||||||
|
microseconds=self.microseconds))
|
||||||
|
if self.weekday:
|
||||||
|
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||||
|
jumpdays = (abs(nth) - 1) * 7
|
||||||
|
if nth > 0:
|
||||||
|
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||||
|
else:
|
||||||
|
jumpdays += (ret.weekday() - weekday) % 7
|
||||||
|
jumpdays *= -1
|
||||||
|
ret += datetime.timedelta(days=jumpdays)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
return self.__add__(other)
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
return self.__neg__().__radd__(other)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
if not isinstance(other, relativedelta):
|
||||||
|
return NotImplemented # In case the other object defines __rsub__
|
||||||
|
return self.__class__(years=self.years - other.years,
|
||||||
|
months=self.months - other.months,
|
||||||
|
days=self.days - other.days,
|
||||||
|
hours=self.hours - other.hours,
|
||||||
|
minutes=self.minutes - other.minutes,
|
||||||
|
seconds=self.seconds - other.seconds,
|
||||||
|
microseconds=self.microseconds - other.microseconds,
|
||||||
|
leapdays=self.leapdays or other.leapdays,
|
||||||
|
year=(self.year if self.year is not None
|
||||||
|
else other.year),
|
||||||
|
month=(self.month if self.month is not None else
|
||||||
|
other.month),
|
||||||
|
day=(self.day if self.day is not None else
|
||||||
|
other.day),
|
||||||
|
weekday=(self.weekday if self.weekday is not None else
|
||||||
|
other.weekday),
|
||||||
|
hour=(self.hour if self.hour is not None else
|
||||||
|
other.hour),
|
||||||
|
minute=(self.minute if self.minute is not None else
|
||||||
|
other.minute),
|
||||||
|
second=(self.second if self.second is not None else
|
||||||
|
other.second),
|
||||||
|
microsecond=(self.microsecond if self.microsecond
|
||||||
|
is not None else
|
||||||
|
other.microsecond))
|
||||||
|
|
||||||
|
def __abs__(self):
|
||||||
|
return self.__class__(years=abs(self.years),
|
||||||
|
months=abs(self.months),
|
||||||
|
days=abs(self.days),
|
||||||
|
hours=abs(self.hours),
|
||||||
|
minutes=abs(self.minutes),
|
||||||
|
seconds=abs(self.seconds),
|
||||||
|
microseconds=abs(self.microseconds),
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __neg__(self):
|
||||||
|
return self.__class__(years=-self.years,
|
||||||
|
months=-self.months,
|
||||||
|
days=-self.days,
|
||||||
|
hours=-self.hours,
|
||||||
|
minutes=-self.minutes,
|
||||||
|
seconds=-self.seconds,
|
||||||
|
microseconds=-self.microseconds,
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return not (not self.years and
|
||||||
|
not self.months and
|
||||||
|
not self.days and
|
||||||
|
not self.hours and
|
||||||
|
not self.minutes and
|
||||||
|
not self.seconds and
|
||||||
|
not self.microseconds and
|
||||||
|
not self.leapdays and
|
||||||
|
self.year is None and
|
||||||
|
self.month is None and
|
||||||
|
self.day is None and
|
||||||
|
self.weekday is None and
|
||||||
|
self.hour is None and
|
||||||
|
self.minute is None and
|
||||||
|
self.second is None and
|
||||||
|
self.microsecond is None)
|
||||||
|
# Compatibility with Python 2.x
|
||||||
|
__nonzero__ = __bool__
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
try:
|
||||||
|
f = float(other)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.__class__(years=int(self.years * f),
|
||||||
|
months=int(self.months * f),
|
||||||
|
days=int(self.days * f),
|
||||||
|
hours=int(self.hours * f),
|
||||||
|
minutes=int(self.minutes * f),
|
||||||
|
seconds=int(self.seconds * f),
|
||||||
|
microseconds=int(self.microseconds * f),
|
||||||
|
leapdays=self.leapdays,
|
||||||
|
year=self.year,
|
||||||
|
month=self.month,
|
||||||
|
day=self.day,
|
||||||
|
weekday=self.weekday,
|
||||||
|
hour=self.hour,
|
||||||
|
minute=self.minute,
|
||||||
|
second=self.second,
|
||||||
|
microsecond=self.microsecond)
|
||||||
|
|
||||||
|
__rmul__ = __mul__
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, relativedelta):
|
||||||
|
return NotImplemented
|
||||||
|
if self.weekday or other.weekday:
|
||||||
|
if not self.weekday or not other.weekday:
|
||||||
|
return False
|
||||||
|
if self.weekday.weekday != other.weekday.weekday:
|
||||||
|
return False
|
||||||
|
n1, n2 = self.weekday.n, other.weekday.n
|
||||||
|
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||||
|
return False
|
||||||
|
return (self.years == other.years and
|
||||||
|
self.months == other.months and
|
||||||
|
self.days == other.days and
|
||||||
|
self.hours == other.hours and
|
||||||
|
self.minutes == other.minutes and
|
||||||
|
self.seconds == other.seconds and
|
||||||
|
self.microseconds == other.microseconds and
|
||||||
|
self.leapdays == other.leapdays and
|
||||||
|
self.year == other.year and
|
||||||
|
self.month == other.month and
|
||||||
|
self.day == other.day and
|
||||||
|
self.hour == other.hour and
|
||||||
|
self.minute == other.minute and
|
||||||
|
self.second == other.second and
|
||||||
|
self.microsecond == other.microsecond)
|
||||||
|
|
||||||
|
def __hash__(self):
|
||||||
|
return hash((
|
||||||
|
self.weekday,
|
||||||
|
self.years,
|
||||||
|
self.months,
|
||||||
|
self.days,
|
||||||
|
self.hours,
|
||||||
|
self.minutes,
|
||||||
|
self.seconds,
|
||||||
|
self.microseconds,
|
||||||
|
self.leapdays,
|
||||||
|
self.year,
|
||||||
|
self.month,
|
||||||
|
self.day,
|
||||||
|
self.hour,
|
||||||
|
self.minute,
|
||||||
|
self.second,
|
||||||
|
self.microsecond,
|
||||||
|
))
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
def __div__(self, other):
|
||||||
|
try:
|
||||||
|
reciprocal = 1 / float(other)
|
||||||
|
except TypeError:
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return self.__mul__(reciprocal)
|
||||||
|
|
||||||
|
__truediv__ = __div__
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
l = []
|
||||||
|
for attr in ["years", "months", "days", "leapdays",
|
||||||
|
"hours", "minutes", "seconds", "microseconds"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value:
|
||||||
|
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
||||||
|
for attr in ["year", "month", "day", "weekday",
|
||||||
|
"hour", "minute", "second", "microsecond"]:
|
||||||
|
value = getattr(self, attr)
|
||||||
|
if value is not None:
|
||||||
|
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
||||||
|
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
||||||
|
attrs=", ".join(l))
|
||||||
|
|
||||||
|
|
||||||
|
def _sign(x):
|
||||||
|
return int(copysign(1, x))
|
||||||
|
|
||||||
|
# vim:ts=4:sw=4:et
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from .tz import *
|
||||||
|
from .tz import __doc__
|
||||||
|
|
||||||
|
#: Convenience constant providing a :class:`tzutc()` instance
|
||||||
|
#:
|
||||||
|
#: .. versionadded:: 2.7.0
|
||||||
|
UTC = tzutc()
|
||||||
|
|
||||||
|
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||||
|
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||||
|
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||||
|
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||||
|
|
||||||
|
|
||||||
|
class DeprecatedTzFormatWarning(Warning):
|
||||||
|
"""Warning raised when time zones are parsed from deprecated formats."""
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
from six import PY3
|
||||||
|
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta, tzinfo
|
||||||
|
|
||||||
|
|
||||||
|
ZERO = timedelta(0)
|
||||||
|
|
||||||
|
__all__ = ['tzname_in_python2', 'enfold']
|
||||||
|
|
||||||
|
|
||||||
|
def tzname_in_python2(namefunc):
|
||||||
|
"""Change unicode output into bytestrings in Python 2
|
||||||
|
|
||||||
|
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||||
|
to unicode strings
|
||||||
|
"""
|
||||||
|
def adjust_encoding(*args, **kwargs):
|
||||||
|
name = namefunc(*args, **kwargs)
|
||||||
|
if name is not None and not PY3:
|
||||||
|
name = name.encode()
|
||||||
|
|
||||||
|
return name
|
||||||
|
|
||||||
|
return adjust_encoding
|
||||||
|
|
||||||
|
|
||||||
|
# The following is adapted from Alexander Belopolsky's tz library
|
||||||
|
# https://github.com/abalkin/tz
|
||||||
|
if hasattr(datetime, 'fold'):
|
||||||
|
# This is the pre-python 3.6 fold situation
|
||||||
|
def enfold(dt, fold=1):
|
||||||
|
"""
|
||||||
|
Provides a unified interface for assigning the ``fold`` attribute to
|
||||||
|
datetimes both before and after the implementation of PEP-495.
|
||||||
|
|
||||||
|
:param fold:
|
||||||
|
The value for the ``fold`` attribute in the returned datetime. This
|
||||||
|
should be either 0 or 1.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||||
|
``fold`` for all versions of Python. In versions prior to
|
||||||
|
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||||
|
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||||
|
attribute added, if ``fold`` is 1.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
return dt.replace(fold=fold)
|
||||||
|
|
||||||
|
else:
|
||||||
|
class _DatetimeWithFold(datetime):
|
||||||
|
"""
|
||||||
|
This is a class designed to provide a PEP 495-compliant interface for
|
||||||
|
Python versions before 3.6. It is used only for dates in a fold, so
|
||||||
|
the ``fold`` attribute is fixed at ``1``.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
__slots__ = ()
|
||||||
|
|
||||||
|
def replace(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return a datetime with the same attributes, except for those
|
||||||
|
attributes given new values by whichever keyword arguments are
|
||||||
|
specified. Note that tzinfo=None can be specified to create a naive
|
||||||
|
datetime from an aware datetime with no conversion of date and time
|
||||||
|
data.
|
||||||
|
|
||||||
|
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
||||||
|
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
||||||
|
"""
|
||||||
|
argnames = (
|
||||||
|
'year', 'month', 'day', 'hour', 'minute', 'second',
|
||||||
|
'microsecond', 'tzinfo'
|
||||||
|
)
|
||||||
|
|
||||||
|
for arg, argname in zip(args, argnames):
|
||||||
|
if argname in kwargs:
|
||||||
|
raise TypeError('Duplicate argument: {}'.format(argname))
|
||||||
|
|
||||||
|
kwargs[argname] = arg
|
||||||
|
|
||||||
|
for argname in argnames:
|
||||||
|
if argname not in kwargs:
|
||||||
|
kwargs[argname] = getattr(self, argname)
|
||||||
|
|
||||||
|
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
||||||
|
|
||||||
|
return dt_class(**kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fold(self):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def enfold(dt, fold=1):
|
||||||
|
"""
|
||||||
|
Provides a unified interface for assigning the ``fold`` attribute to
|
||||||
|
datetimes both before and after the implementation of PEP-495.
|
||||||
|
|
||||||
|
:param fold:
|
||||||
|
The value for the ``fold`` attribute in the returned datetime. This
|
||||||
|
should be either 0 or 1.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||||
|
``fold`` for all versions of Python. In versions prior to
|
||||||
|
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||||
|
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||||
|
attribute added, if ``fold`` is 1.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
if getattr(dt, 'fold', 0) == fold:
|
||||||
|
return dt
|
||||||
|
|
||||||
|
args = dt.timetuple()[:6]
|
||||||
|
args += (dt.microsecond, dt.tzinfo)
|
||||||
|
|
||||||
|
if fold:
|
||||||
|
return _DatetimeWithFold(*args)
|
||||||
|
else:
|
||||||
|
return datetime(*args)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_fromutc_inputs(f):
|
||||||
|
"""
|
||||||
|
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
||||||
|
object and that ``self`` is attached as its ``tzinfo``.
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def fromutc(self, dt):
|
||||||
|
if not isinstance(dt, datetime):
|
||||||
|
raise TypeError("fromutc() requires a datetime argument")
|
||||||
|
if dt.tzinfo is not self:
|
||||||
|
raise ValueError("dt.tzinfo is not self")
|
||||||
|
|
||||||
|
return f(self, dt)
|
||||||
|
|
||||||
|
return fromutc
|
||||||
|
|
||||||
|
|
||||||
|
class _tzinfo(tzinfo):
|
||||||
|
"""
|
||||||
|
Base class for all ``dateutil`` ``tzinfo`` objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def is_ambiguous(self, dt):
|
||||||
|
"""
|
||||||
|
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||||
|
zone.
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=self)
|
||||||
|
|
||||||
|
wall_0 = enfold(dt, fold=0)
|
||||||
|
wall_1 = enfold(dt, fold=1)
|
||||||
|
|
||||||
|
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
||||||
|
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
||||||
|
|
||||||
|
return same_dt and not same_offset
|
||||||
|
|
||||||
|
def _fold_status(self, dt_utc, dt_wall):
|
||||||
|
"""
|
||||||
|
Determine the fold status of a "wall" datetime, given a representation
|
||||||
|
of the same datetime as a (naive) UTC datetime. This is calculated based
|
||||||
|
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
||||||
|
datetimes, and that this offset is the actual number of hours separating
|
||||||
|
``dt_utc`` and ``dt_wall``.
|
||||||
|
|
||||||
|
:param dt_utc:
|
||||||
|
Representation of the datetime as UTC
|
||||||
|
|
||||||
|
:param dt_wall:
|
||||||
|
Representation of the datetime as "wall time". This parameter must
|
||||||
|
either have a `fold` attribute or have a fold-naive
|
||||||
|
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
||||||
|
fail.
|
||||||
|
"""
|
||||||
|
if self.is_ambiguous(dt_wall):
|
||||||
|
delta_wall = dt_wall - dt_utc
|
||||||
|
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
||||||
|
else:
|
||||||
|
_fold = 0
|
||||||
|
|
||||||
|
return _fold
|
||||||
|
|
||||||
|
def _fold(self, dt):
|
||||||
|
return getattr(dt, 'fold', 0)
|
||||||
|
|
||||||
|
def _fromutc(self, dt):
|
||||||
|
"""
|
||||||
|
Given a timezone-aware datetime in a given timezone, calculates a
|
||||||
|
timezone-aware datetime in a new timezone.
|
||||||
|
|
||||||
|
Since this is the one time that we *know* we have an unambiguous
|
||||||
|
datetime object, we take this opportunity to determine whether the
|
||||||
|
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||||
|
occurence, chronologically, of the ambiguous datetime).
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A timezone-aware :class:`datetime.datetime` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Re-implement the algorithm from Python's datetime.py
|
||||||
|
dtoff = dt.utcoffset()
|
||||||
|
if dtoff is None:
|
||||||
|
raise ValueError("fromutc() requires a non-None utcoffset() "
|
||||||
|
"result")
|
||||||
|
|
||||||
|
# The original datetime.py code assumes that `dst()` defaults to
|
||||||
|
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
||||||
|
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
||||||
|
dtdst = dt.dst()
|
||||||
|
if dtdst is None:
|
||||||
|
raise ValueError("fromutc() requires a non-None dst() result")
|
||||||
|
delta = dtoff - dtdst
|
||||||
|
|
||||||
|
dt += delta
|
||||||
|
# Set fold=1 so we can default to being in the fold for
|
||||||
|
# ambiguous dates.
|
||||||
|
dtdst = enfold(dt, fold=1).dst()
|
||||||
|
if dtdst is None:
|
||||||
|
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
||||||
|
"results; cannot convert")
|
||||||
|
return dt + dtdst
|
||||||
|
|
||||||
|
@_validate_fromutc_inputs
|
||||||
|
def fromutc(self, dt):
|
||||||
|
"""
|
||||||
|
Given a timezone-aware datetime in a given timezone, calculates a
|
||||||
|
timezone-aware datetime in a new timezone.
|
||||||
|
|
||||||
|
Since this is the one time that we *know* we have an unambiguous
|
||||||
|
datetime object, we take this opportunity to determine whether the
|
||||||
|
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||||
|
occurance, chronologically, of the ambiguous datetime).
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A timezone-aware :class:`datetime.datetime` object.
|
||||||
|
"""
|
||||||
|
dt_wall = self._fromutc(dt)
|
||||||
|
|
||||||
|
# Calculate the fold status given the two datetimes.
|
||||||
|
_fold = self._fold_status(dt, dt_wall)
|
||||||
|
|
||||||
|
# Set the default fold value for ambiguous dates
|
||||||
|
return enfold(dt_wall, fold=_fold)
|
||||||
|
|
||||||
|
|
||||||
|
class tzrangebase(_tzinfo):
|
||||||
|
"""
|
||||||
|
This is an abstract base class for time zones represented by an annual
|
||||||
|
transition into and out of DST. Child classes should implement the following
|
||||||
|
methods:
|
||||||
|
|
||||||
|
* ``__init__(self, *args, **kwargs)``
|
||||||
|
* ``transitions(self, year)`` - this is expected to return a tuple of
|
||||||
|
datetimes representing the DST on and off transitions in standard
|
||||||
|
time.
|
||||||
|
|
||||||
|
A fully initialized ``tzrangebase`` subclass should also provide the
|
||||||
|
following attributes:
|
||||||
|
* ``hasdst``: Boolean whether or not the zone uses DST.
|
||||||
|
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
||||||
|
representing the respective UTC offsets.
|
||||||
|
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
||||||
|
abbreviations in DST and STD, respectively.
|
||||||
|
* ``_hasdst``: Whether or not the zone has DST.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError('tzrangebase is an abstract base class')
|
||||||
|
|
||||||
|
def utcoffset(self, dt):
|
||||||
|
isdst = self._isdst(dt)
|
||||||
|
|
||||||
|
if isdst is None:
|
||||||
|
return None
|
||||||
|
elif isdst:
|
||||||
|
return self._dst_offset
|
||||||
|
else:
|
||||||
|
return self._std_offset
|
||||||
|
|
||||||
|
def dst(self, dt):
|
||||||
|
isdst = self._isdst(dt)
|
||||||
|
|
||||||
|
if isdst is None:
|
||||||
|
return None
|
||||||
|
elif isdst:
|
||||||
|
return self._dst_base_offset
|
||||||
|
else:
|
||||||
|
return ZERO
|
||||||
|
|
||||||
|
@tzname_in_python2
|
||||||
|
def tzname(self, dt):
|
||||||
|
if self._isdst(dt):
|
||||||
|
return self._dst_abbr
|
||||||
|
else:
|
||||||
|
return self._std_abbr
|
||||||
|
|
||||||
|
def fromutc(self, dt):
|
||||||
|
""" Given a datetime in UTC, return local time """
|
||||||
|
if not isinstance(dt, datetime):
|
||||||
|
raise TypeError("fromutc() requires a datetime argument")
|
||||||
|
|
||||||
|
if dt.tzinfo is not self:
|
||||||
|
raise ValueError("dt.tzinfo is not self")
|
||||||
|
|
||||||
|
# Get transitions - if there are none, fixed offset
|
||||||
|
transitions = self.transitions(dt.year)
|
||||||
|
if transitions is None:
|
||||||
|
return dt + self.utcoffset(dt)
|
||||||
|
|
||||||
|
# Get the transition times in UTC
|
||||||
|
dston, dstoff = transitions
|
||||||
|
|
||||||
|
dston -= self._std_offset
|
||||||
|
dstoff -= self._std_offset
|
||||||
|
|
||||||
|
utc_transitions = (dston, dstoff)
|
||||||
|
dt_utc = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
||||||
|
|
||||||
|
if isdst:
|
||||||
|
dt_wall = dt + self._dst_offset
|
||||||
|
else:
|
||||||
|
dt_wall = dt + self._std_offset
|
||||||
|
|
||||||
|
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
||||||
|
|
||||||
|
return enfold(dt_wall, fold=_fold)
|
||||||
|
|
||||||
|
def is_ambiguous(self, dt):
|
||||||
|
"""
|
||||||
|
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||||
|
zone.
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||||
|
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
"""
|
||||||
|
if not self.hasdst:
|
||||||
|
return False
|
||||||
|
|
||||||
|
start, end = self.transitions(dt.year)
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
return (end <= dt < end + self._dst_base_offset)
|
||||||
|
|
||||||
|
def _isdst(self, dt):
|
||||||
|
if not self.hasdst:
|
||||||
|
return False
|
||||||
|
elif dt is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
transitions = self.transitions(dt.year)
|
||||||
|
|
||||||
|
if transitions is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
isdst = self._naive_isdst(dt, transitions)
|
||||||
|
|
||||||
|
# Handle ambiguous dates
|
||||||
|
if not isdst and self.is_ambiguous(dt):
|
||||||
|
return not self._fold(dt)
|
||||||
|
else:
|
||||||
|
return isdst
|
||||||
|
|
||||||
|
def _naive_isdst(self, dt, transitions):
|
||||||
|
dston, dstoff = transitions
|
||||||
|
|
||||||
|
dt = dt.replace(tzinfo=None)
|
||||||
|
|
||||||
|
if dston < dstoff:
|
||||||
|
isdst = dston <= dt < dstoff
|
||||||
|
else:
|
||||||
|
isdst = not dstoff <= dt < dston
|
||||||
|
|
||||||
|
return isdst
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dst_base_offset(self):
|
||||||
|
return self._dst_offset - self._std_offset
|
||||||
|
|
||||||
|
__hash__ = None
|
||||||
|
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self == other)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s(...)" % self.__class__.__name__
|
||||||
|
|
||||||
|
__reduce__ = object.__reduce__
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class _TzSingleton(type):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instance = None
|
||||||
|
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def __call__(cls):
|
||||||
|
if cls.__instance is None:
|
||||||
|
cls.__instance = super(_TzSingleton, cls).__call__()
|
||||||
|
return cls.__instance
|
||||||
|
|
||||||
|
class _TzFactory(type):
|
||||||
|
def instance(cls, *args, **kwargs):
|
||||||
|
"""Alternate constructor that returns a fresh instance"""
|
||||||
|
return type.__call__(cls, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class _TzOffsetFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = {}
|
||||||
|
|
||||||
|
def __call__(cls, name, offset):
|
||||||
|
if isinstance(offset, timedelta):
|
||||||
|
key = (name, offset.total_seconds())
|
||||||
|
else:
|
||||||
|
key = (name, offset)
|
||||||
|
|
||||||
|
instance = cls.__instances.get(key, None)
|
||||||
|
if instance is None:
|
||||||
|
instance = cls.__instances.setdefault(key,
|
||||||
|
cls.instance(name, offset))
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class _TzStrFactory(_TzFactory):
|
||||||
|
def __init__(cls, *args, **kwargs):
|
||||||
|
cls.__instances = {}
|
||||||
|
|
||||||
|
def __call__(cls, s, posix_offset=False):
|
||||||
|
key = (s, posix_offset)
|
||||||
|
instance = cls.__instances.get(key, None)
|
||||||
|
|
||||||
|
if instance is None:
|
||||||
|
instance = cls.__instances.setdefault(key,
|
||||||
|
cls.instance(s, posix_offset))
|
||||||
|
return instance
|
||||||
|
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
# This code was originally contributed by Jeffrey Harris.
|
||||||
|
import datetime
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from six.moves import winreg
|
||||||
|
from six import text_type
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ctypes
|
||||||
|
from ctypes import wintypes
|
||||||
|
except ValueError:
|
||||||
|
# ValueError is raised on non-Windows systems for some horrible reason.
|
||||||
|
raise ImportError("Running tzwin on non-Windows system")
|
||||||
|
|
||||||
|
from ._common import tzrangebase
|
||||||
|
|
||||||
|
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
||||||
|
|
||||||
|
ONEWEEK = datetime.timedelta(7)
|
||||||
|
|
||||||
|
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||||
|
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||||
|
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||||
|
|
||||||
|
|
||||||
|
def _settzkeyname():
|
||||||
|
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||||
|
try:
|
||||||
|
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||||
|
TZKEYNAME = TZKEYNAMENT
|
||||||
|
except WindowsError:
|
||||||
|
TZKEYNAME = TZKEYNAME9X
|
||||||
|
handle.Close()
|
||||||
|
return TZKEYNAME
|
||||||
|
|
||||||
|
|
||||||
|
TZKEYNAME = _settzkeyname()
|
||||||
|
|
||||||
|
|
||||||
|
class tzres(object):
|
||||||
|
"""
|
||||||
|
Class for accessing `tzres.dll`, which contains timezone name related
|
||||||
|
resources.
|
||||||
|
|
||||||
|
.. versionadded:: 2.5.0
|
||||||
|
"""
|
||||||
|
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
||||||
|
|
||||||
|
def __init__(self, tzres_loc='tzres.dll'):
|
||||||
|
# Load the user32 DLL so we can load strings from tzres
|
||||||
|
user32 = ctypes.WinDLL('user32')
|
||||||
|
|
||||||
|
# Specify the LoadStringW function
|
||||||
|
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
||||||
|
wintypes.UINT,
|
||||||
|
wintypes.LPWSTR,
|
||||||
|
ctypes.c_int)
|
||||||
|
|
||||||
|
self.LoadStringW = user32.LoadStringW
|
||||||
|
self._tzres = ctypes.WinDLL(tzres_loc)
|
||||||
|
self.tzres_loc = tzres_loc
|
||||||
|
|
||||||
|
def load_name(self, offset):
|
||||||
|
"""
|
||||||
|
Load a timezone name from a DLL offset (integer).
|
||||||
|
|
||||||
|
>>> from dateutil.tzwin import tzres
|
||||||
|
>>> tzr = tzres()
|
||||||
|
>>> print(tzr.load_name(112))
|
||||||
|
'Eastern Standard Time'
|
||||||
|
|
||||||
|
:param offset:
|
||||||
|
A positive integer value referring to a string from the tzres dll.
|
||||||
|
|
||||||
|
..note:
|
||||||
|
Offsets found in the registry are generally of the form
|
||||||
|
`@tzres.dll,-114`. The offset in this case if 114, not -114.
|
||||||
|
|
||||||
|
"""
|
||||||
|
resource = self.p_wchar()
|
||||||
|
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
||||||
|
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
||||||
|
return resource[:nchar]
|
||||||
|
|
||||||
|
def name_from_string(self, tzname_str):
|
||||||
|
"""
|
||||||
|
Parse strings as returned from the Windows registry into the time zone
|
||||||
|
name as defined in the registry.
|
||||||
|
|
||||||
|
>>> from dateutil.tzwin import tzres
|
||||||
|
>>> tzr = tzres()
|
||||||
|
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
||||||
|
'Dateline Daylight Time'
|
||||||
|
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
||||||
|
'Eastern Standard Time'
|
||||||
|
|
||||||
|
:param tzname_str:
|
||||||
|
A timezone name string as returned from a Windows registry key.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns the localized timezone string from tzres.dll if the string
|
||||||
|
is of the form `@tzres.dll,-offset`, else returns the input string.
|
||||||
|
"""
|
||||||
|
if not tzname_str.startswith('@'):
|
||||||
|
return tzname_str
|
||||||
|
|
||||||
|
name_splt = tzname_str.split(',-')
|
||||||
|
try:
|
||||||
|
offset = int(name_splt[1])
|
||||||
|
except:
|
||||||
|
raise ValueError("Malformed timezone string.")
|
||||||
|
|
||||||
|
return self.load_name(offset)
|
||||||
|
|
||||||
|
|
||||||
|
class tzwinbase(tzrangebase):
|
||||||
|
"""tzinfo class based on win32's timezones available in the registry."""
|
||||||
|
def __init__(self):
|
||||||
|
raise NotImplementedError('tzwinbase is an abstract base class')
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
# Compare on all relevant dimensions, including name.
|
||||||
|
if not isinstance(other, tzwinbase):
|
||||||
|
return NotImplemented
|
||||||
|
|
||||||
|
return (self._std_offset == other._std_offset and
|
||||||
|
self._dst_offset == other._dst_offset and
|
||||||
|
self._stddayofweek == other._stddayofweek and
|
||||||
|
self._dstdayofweek == other._dstdayofweek and
|
||||||
|
self._stdweeknumber == other._stdweeknumber and
|
||||||
|
self._dstweeknumber == other._dstweeknumber and
|
||||||
|
self._stdhour == other._stdhour and
|
||||||
|
self._dsthour == other._dsthour and
|
||||||
|
self._stdminute == other._stdminute and
|
||||||
|
self._dstminute == other._dstminute and
|
||||||
|
self._std_abbr == other._std_abbr and
|
||||||
|
self._dst_abbr == other._dst_abbr)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def list():
|
||||||
|
"""Return a list of all time zones known to the system."""
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
||||||
|
result = [winreg.EnumKey(tzkey, i)
|
||||||
|
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||||
|
return result
|
||||||
|
|
||||||
|
def display(self):
|
||||||
|
return self._display
|
||||||
|
|
||||||
|
def transitions(self, year):
|
||||||
|
"""
|
||||||
|
For a given year, get the DST on and off transition times, expressed
|
||||||
|
always on the standard time side. For zones with no transitions, this
|
||||||
|
function returns ``None``.
|
||||||
|
|
||||||
|
:param year:
|
||||||
|
The year whose transitions you would like to query.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
||||||
|
``(dston, dstoff)`` for zones with an annual DST transition, or
|
||||||
|
``None`` for fixed offset zones.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not self.hasdst:
|
||||||
|
return None
|
||||||
|
|
||||||
|
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
||||||
|
self._dsthour, self._dstminute,
|
||||||
|
self._dstweeknumber)
|
||||||
|
|
||||||
|
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
||||||
|
self._stdhour, self._stdminute,
|
||||||
|
self._stdweeknumber)
|
||||||
|
|
||||||
|
# Ambiguous dates default to the STD side
|
||||||
|
dstoff -= self._dst_base_offset
|
||||||
|
|
||||||
|
return dston, dstoff
|
||||||
|
|
||||||
|
def _get_hasdst(self):
|
||||||
|
return self._dstmonth != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dst_base_offset(self):
|
||||||
|
return self._dst_base_offset_
|
||||||
|
|
||||||
|
|
||||||
|
class tzwin(tzwinbase):
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self._name = name
|
||||||
|
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
||||||
|
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||||
|
keydict = valuestodict(tzkey)
|
||||||
|
|
||||||
|
self._std_abbr = keydict["Std"]
|
||||||
|
self._dst_abbr = keydict["Dlt"]
|
||||||
|
|
||||||
|
self._display = keydict["Display"]
|
||||||
|
|
||||||
|
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||||
|
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||||
|
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||||
|
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
||||||
|
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||||
|
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||||
|
|
||||||
|
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||||
|
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||||
|
(self._stdmonth,
|
||||||
|
self._stddayofweek, # Sunday = 0
|
||||||
|
self._stdweeknumber, # Last = 5
|
||||||
|
self._stdhour,
|
||||||
|
self._stdminute) = tup[4:9]
|
||||||
|
|
||||||
|
(self._dstmonth,
|
||||||
|
self._dstdayofweek, # Sunday = 0
|
||||||
|
self._dstweeknumber, # Last = 5
|
||||||
|
self._dsthour,
|
||||||
|
self._dstminute) = tup[12:17]
|
||||||
|
|
||||||
|
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||||
|
self.hasdst = self._get_hasdst()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "tzwin(%s)" % repr(self._name)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (self.__class__, (self._name,))
|
||||||
|
|
||||||
|
|
||||||
|
class tzwinlocal(tzwinbase):
|
||||||
|
def __init__(self):
|
||||||
|
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||||
|
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||||
|
keydict = valuestodict(tzlocalkey)
|
||||||
|
|
||||||
|
self._std_abbr = keydict["StandardName"]
|
||||||
|
self._dst_abbr = keydict["DaylightName"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
||||||
|
sn=self._std_abbr)
|
||||||
|
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||||
|
_keydict = valuestodict(tzkey)
|
||||||
|
self._display = _keydict["Display"]
|
||||||
|
except OSError:
|
||||||
|
self._display = None
|
||||||
|
|
||||||
|
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||||
|
dstoffset = stdoffset-keydict["DaylightBias"]
|
||||||
|
|
||||||
|
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||||
|
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||||
|
|
||||||
|
# For reasons unclear, in this particular key, the day of week has been
|
||||||
|
# moved to the END of the SYSTEMTIME structure.
|
||||||
|
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||||
|
|
||||||
|
(self._stdmonth,
|
||||||
|
self._stdweeknumber, # Last = 5
|
||||||
|
self._stdhour,
|
||||||
|
self._stdminute) = tup[1:5]
|
||||||
|
|
||||||
|
self._stddayofweek = tup[7]
|
||||||
|
|
||||||
|
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||||
|
|
||||||
|
(self._dstmonth,
|
||||||
|
self._dstweeknumber, # Last = 5
|
||||||
|
self._dsthour,
|
||||||
|
self._dstminute) = tup[1:5]
|
||||||
|
|
||||||
|
self._dstdayofweek = tup[7]
|
||||||
|
|
||||||
|
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||||
|
self.hasdst = self._get_hasdst()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "tzwinlocal()"
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
# str will return the standard name, not the daylight name.
|
||||||
|
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
return (self.__class__, ())
|
||||||
|
|
||||||
|
|
||||||
|
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||||
|
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
||||||
|
first = datetime.datetime(year, month, 1, hour, minute)
|
||||||
|
|
||||||
|
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
||||||
|
# Because 7 % 7 = 0
|
||||||
|
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
||||||
|
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
||||||
|
if (wd.month != month):
|
||||||
|
wd -= ONEWEEK
|
||||||
|
|
||||||
|
return wd
|
||||||
|
|
||||||
|
|
||||||
|
def valuestodict(key):
|
||||||
|
"""Convert a registry key's values to a dictionary."""
|
||||||
|
dout = {}
|
||||||
|
size = winreg.QueryInfoKey(key)[1]
|
||||||
|
tz_res = None
|
||||||
|
|
||||||
|
for i in range(size):
|
||||||
|
key_name, value, dtype = winreg.EnumValue(key, i)
|
||||||
|
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
||||||
|
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
||||||
|
# that to a proper signed integer
|
||||||
|
if value & (1 << 31):
|
||||||
|
value = value - (1 << 32)
|
||||||
|
elif dtype == winreg.REG_SZ:
|
||||||
|
# If it's a reference to the tzres DLL, load the actual string
|
||||||
|
if value.startswith('@tzres'):
|
||||||
|
tz_res = tz_res or tzres()
|
||||||
|
value = tz_res.name_from_string(value)
|
||||||
|
|
||||||
|
value = value.rstrip('\x00') # Remove trailing nulls
|
||||||
|
|
||||||
|
dout[key_name] = value
|
||||||
|
|
||||||
|
return dout
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# tzwin has moved to dateutil.tz.win
|
||||||
|
from .tz.win import *
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
This module offers general convenience and utility functions for dealing with
|
||||||
|
datetimes.
|
||||||
|
|
||||||
|
.. versionadded:: 2.7.0
|
||||||
|
"""
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
|
||||||
|
def today(tzinfo=None):
|
||||||
|
"""
|
||||||
|
Returns a :py:class:`datetime` representing the current day at midnight
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The time zone to attach (also used to determine the current day).
|
||||||
|
|
||||||
|
:return:
|
||||||
|
A :py:class:`datetime.datetime` object representing the current day
|
||||||
|
at midnight.
|
||||||
|
"""
|
||||||
|
|
||||||
|
dt = datetime.now(tzinfo)
|
||||||
|
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||||
|
|
||||||
|
|
||||||
|
def default_tzinfo(dt, tzinfo):
|
||||||
|
"""
|
||||||
|
Sets the the ``tzinfo`` parameter on naive datetimes only
|
||||||
|
|
||||||
|
This is useful for example when you are provided a datetime that may have
|
||||||
|
either an implicit or explicit time zone, such as when parsing a time zone
|
||||||
|
string.
|
||||||
|
|
||||||
|
.. doctest::
|
||||||
|
|
||||||
|
>>> from dateutil.tz import tzoffset
|
||||||
|
>>> from dateutil.parser import parse
|
||||||
|
>>> from dateutil.utils import default_tzinfo
|
||||||
|
>>> dflt_tz = tzoffset("EST", -18000)
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00+00:00
|
||||||
|
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||||
|
2014-01-01 12:30:00-05:00
|
||||||
|
|
||||||
|
:param dt:
|
||||||
|
The datetime on which to replace the time zone
|
||||||
|
|
||||||
|
:param tzinfo:
|
||||||
|
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||||
|
``dt`` if (and only if) it is naive.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns an aware :py:class:`datetime.datetime`.
|
||||||
|
"""
|
||||||
|
if dt.tzinfo is not None:
|
||||||
|
return dt
|
||||||
|
else:
|
||||||
|
return dt.replace(tzinfo=tzinfo)
|
||||||
|
|
||||||
|
|
||||||
|
def within_delta(dt1, dt2, delta):
|
||||||
|
"""
|
||||||
|
Useful for comparing two datetimes that may a negilible difference
|
||||||
|
to be considered equal.
|
||||||
|
"""
|
||||||
|
delta = abs(delta)
|
||||||
|
difference = dt1 - dt2
|
||||||
|
return -delta <= difference <= delta
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import warnings
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tarfile import TarFile
|
||||||
|
from pkgutil import get_data
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
|
from dateutil.tz import tzfile as _tzfile
|
||||||
|
|
||||||
|
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||||
|
|
||||||
|
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||||
|
METADATA_FN = 'METADATA'
|
||||||
|
|
||||||
|
|
||||||
|
class tzfile(_tzfile):
|
||||||
|
def __reduce__(self):
|
||||||
|
return (gettz, (self._filename,))
|
||||||
|
|
||||||
|
|
||||||
|
def getzoneinfofile_stream():
|
||||||
|
try:
|
||||||
|
return BytesIO(get_data(__name__, ZONEFILENAME))
|
||||||
|
except IOError as e: # TODO switch to FileNotFoundError?
|
||||||
|
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ZoneInfoFile(object):
|
||||||
|
def __init__(self, zonefile_stream=None):
|
||||||
|
if zonefile_stream is not None:
|
||||||
|
with TarFile.open(fileobj=zonefile_stream) as tf:
|
||||||
|
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
||||||
|
for zf in tf.getmembers()
|
||||||
|
if zf.isfile() and zf.name != METADATA_FN}
|
||||||
|
# deal with links: They'll point to their parent object. Less
|
||||||
|
# waste of memory
|
||||||
|
links = {zl.name: self.zones[zl.linkname]
|
||||||
|
for zl in tf.getmembers() if
|
||||||
|
zl.islnk() or zl.issym()}
|
||||||
|
self.zones.update(links)
|
||||||
|
try:
|
||||||
|
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
||||||
|
metadata_str = metadata_json.read().decode('UTF-8')
|
||||||
|
self.metadata = json.loads(metadata_str)
|
||||||
|
except KeyError:
|
||||||
|
# no metadata in tar file
|
||||||
|
self.metadata = None
|
||||||
|
else:
|
||||||
|
self.zones = {}
|
||||||
|
self.metadata = None
|
||||||
|
|
||||||
|
def get(self, name, default=None):
|
||||||
|
"""
|
||||||
|
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
||||||
|
for retrieving zones from the zone dictionary.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
The name of the zone to retrieve. (Generally IANA zone names)
|
||||||
|
|
||||||
|
:param default:
|
||||||
|
The value to return in the event of a missing key.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6.0
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.zones.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
|
# The current API has gettz as a module function, although in fact it taps into
|
||||||
|
# a stateful class. So as a workaround for now, without changing the API, we
|
||||||
|
# will create a new "global" class instance the first time a user requests a
|
||||||
|
# timezone. Ugly, but adheres to the api.
|
||||||
|
#
|
||||||
|
# TODO: Remove after deprecation period.
|
||||||
|
_CLASS_ZONE_INSTANCE = []
|
||||||
|
|
||||||
|
|
||||||
|
def get_zonefile_instance(new_instance=False):
|
||||||
|
"""
|
||||||
|
This is a convenience function which provides a :class:`ZoneInfoFile`
|
||||||
|
instance using the data provided by the ``dateutil`` package. By default, it
|
||||||
|
caches a single instance of the ZoneInfoFile object and returns that.
|
||||||
|
|
||||||
|
:param new_instance:
|
||||||
|
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
||||||
|
used as the cached instance for the next call. Otherwise, new instances
|
||||||
|
are created only as necessary.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`ZoneInfoFile` object.
|
||||||
|
|
||||||
|
.. versionadded:: 2.6
|
||||||
|
"""
|
||||||
|
if new_instance:
|
||||||
|
zif = None
|
||||||
|
else:
|
||||||
|
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
||||||
|
|
||||||
|
if zif is None:
|
||||||
|
zif = ZoneInfoFile(getzoneinfofile_stream())
|
||||||
|
|
||||||
|
get_zonefile_instance._cached_instance = zif
|
||||||
|
|
||||||
|
return zif
|
||||||
|
|
||||||
|
|
||||||
|
def gettz(name):
|
||||||
|
"""
|
||||||
|
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
||||||
|
with dateutil.
|
||||||
|
|
||||||
|
:param name:
|
||||||
|
An IANA-style time zone name, as found in the zoneinfo file.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
It is generally inadvisable to use this function, and it is only
|
||||||
|
provided for API compatibility with earlier versions. This is *not*
|
||||||
|
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
||||||
|
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
||||||
|
for accessing the dateutil-specific zoneinfo (which may be out of
|
||||||
|
date compared to the system zoneinfo).
|
||||||
|
|
||||||
|
.. deprecated:: 2.6
|
||||||
|
If you need to use a specific zoneinfofile over the system zoneinfo,
|
||||||
|
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
||||||
|
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
||||||
|
|
||||||
|
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
||||||
|
dateutil-provided zoneinfo.
|
||||||
|
"""
|
||||||
|
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
||||||
|
"to use the dateutil-provided zoneinfo files, instantiate a "
|
||||||
|
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
||||||
|
"instead. See the documentation for details.",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
|
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||||
|
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||||
|
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
||||||
|
|
||||||
|
|
||||||
|
def gettz_db_metadata():
|
||||||
|
""" Get the zonefile metadata
|
||||||
|
|
||||||
|
See `zonefile_metadata`_
|
||||||
|
|
||||||
|
:returns:
|
||||||
|
A dictionary with the database metadata
|
||||||
|
|
||||||
|
.. deprecated:: 2.6
|
||||||
|
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
||||||
|
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
||||||
|
"""
|
||||||
|
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
||||||
|
"versions, to use the dateutil-provided zoneinfo files, "
|
||||||
|
"ZoneInfoFile object and query the 'metadata' attribute "
|
||||||
|
"instead. See the documentation for details.",
|
||||||
|
DeprecationWarning)
|
||||||
|
|
||||||
|
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||||
|
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||||
|
return _CLASS_ZONE_INSTANCE[0].metadata
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import json
|
||||||
|
from subprocess import check_call
|
||||||
|
from tarfile import TarFile
|
||||||
|
|
||||||
|
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||||
|
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||||
|
|
||||||
|
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
tmpdir = tempfile.mkdtemp()
|
||||||
|
zonedir = os.path.join(tmpdir, "zoneinfo")
|
||||||
|
moduledir = os.path.dirname(__file__)
|
||||||
|
try:
|
||||||
|
with TarFile.open(filename) as tf:
|
||||||
|
for name in zonegroups:
|
||||||
|
tf.extract(name, tmpdir)
|
||||||
|
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
||||||
|
try:
|
||||||
|
check_call(["zic", "-d", zonedir] + filepaths)
|
||||||
|
except OSError as e:
|
||||||
|
_print_on_nosuchfile(e)
|
||||||
|
raise
|
||||||
|
# write metadata file
|
||||||
|
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=4, sort_keys=True)
|
||||||
|
target = os.path.join(moduledir, ZONEFILENAME)
|
||||||
|
with TarFile.open(target, "w:%s" % format) as tf:
|
||||||
|
for entry in os.listdir(zonedir):
|
||||||
|
entrypath = os.path.join(zonedir, entry)
|
||||||
|
tf.add(entrypath, entry)
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
def _print_on_nosuchfile(e):
|
||||||
|
"""Print helpful troubleshooting message
|
||||||
|
|
||||||
|
e is an exception raised by subprocess.check_call()
|
||||||
|
|
||||||
|
"""
|
||||||
|
if e.errno == 2:
|
||||||
|
logging.error(
|
||||||
|
"Could not find zic. Perhaps you need to install "
|
||||||
|
"libc-bin or some other package that provides it, "
|
||||||
|
"or it's not in your PATH?")
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""Run the EasyInstall command"""
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from setuptools.command.easy_install import main
|
||||||
|
main()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
Welcome to Kiwi
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/nucleic/kiwi.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/nucleic/kiwi
|
||||||
|
|
||||||
|
Kiwi is an efficient C++ implementation of the Cassowary constraint solving
|
||||||
|
algorithm. Kiwi is an implementation of the algorithm based on the seminal
|
||||||
|
Cassowary paper. It is *not* a refactoring of the original C++ solver. Kiwi
|
||||||
|
has been designed from the ground up to be lightweight and fast. Kiwi ranges
|
||||||
|
from 10x to 500x faster than the original Cassowary solver with typical use
|
||||||
|
cases gaining a 40x improvement. Memory savings are consistently > 5x.
|
||||||
|
|
||||||
|
In addition to the C++ solver, Kiwi ships with hand-rolled Python bindings.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
Metadata-Version: 2.0
|
||||||
|
Name: kiwisolver
|
||||||
|
Version: 1.0.1
|
||||||
|
Summary: A fast implementation of the Cassowary constraint solver
|
||||||
|
Home-page: https://github.com/nucleic/kiwi
|
||||||
|
Author: The Nucleic Development Team
|
||||||
|
Author-email: sccolbert@gmail.com
|
||||||
|
License: UNKNOWN
|
||||||
|
Description-Content-Type: UNKNOWN
|
||||||
|
Platform: UNKNOWN
|
||||||
|
Requires-Dist: setuptools
|
||||||
|
|
||||||
|
Welcome to Kiwi
|
||||||
|
===============
|
||||||
|
|
||||||
|
.. image:: https://travis-ci.org/nucleic/kiwi.svg?branch=master
|
||||||
|
:target: https://travis-ci.org/nucleic/kiwi
|
||||||
|
|
||||||
|
Kiwi is an efficient C++ implementation of the Cassowary constraint solving
|
||||||
|
algorithm. Kiwi is an implementation of the algorithm based on the seminal
|
||||||
|
Cassowary paper. It is *not* a refactoring of the original C++ solver. Kiwi
|
||||||
|
has been designed from the ground up to be lightweight and fast. Kiwi ranges
|
||||||
|
from 10x to 500x faster than the original Cassowary solver with typical use
|
||||||
|
cases gaining a 40x improvement. Memory savings are consistently > 5x.
|
||||||
|
|
||||||
|
In addition to the C++ solver, Kiwi ships with hand-rolled Python bindings.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
kiwisolver-1.0.1.dist-info/DESCRIPTION.rst,sha256=Qy5sjKaN4toH_Q7EUHWgwRVmpxBgeGUwEcyB75zRwSI,676
|
||||||
|
kiwisolver-1.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
kiwisolver-1.0.1.dist-info/METADATA,sha256=bFtjRI423l1EopYow7-FVdw80WP942cTW4I37mnnunU,1006
|
||||||
|
kiwisolver-1.0.1.dist-info/RECORD,,
|
||||||
|
kiwisolver-1.0.1.dist-info/WHEEL,sha256=xLbWRW0PO79-mIB5Y7BBxUY9L97LDdQCs-obTuQSLEg,109
|
||||||
|
kiwisolver-1.0.1.dist-info/metadata.json,sha256=H-tQ6em1_t3jGeivswhlBiHFJ7CBAI6G_RZAsT-QHCs,535
|
||||||
|
kiwisolver-1.0.1.dist-info/top_level.txt,sha256=xqwWj7oSHlpIjcw2QMJb8puTFPdjDBO78AZp9gjTh9c,11
|
||||||
|
kiwisolver.cpython-36m-x86_64-linux-gnu.so,sha256=4GkBV_S_pJ93VLqTL5uPrsTtcIaTx6RIvdblto_AmVg,3860760
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.30.0)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp36-cp36m-manylinux1_x86_64
|
||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"description_content_type": "UNKNOWN", "extensions": {"python.details": {"contacts": [{"email": "sccolbert@gmail.com", "name": "The Nucleic Development Team", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/nucleic/kiwi"}}}, "extras": [], "generator": "bdist_wheel (0.30.0)", "metadata_version": "2.0", "name": "kiwisolver", "run_requires": [{"requires": ["setuptools"]}], "summary": "A fast implementation of the Cassowary constraint solver", "version": "1.0.1"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
kiwisolver
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('mpl_toolkits',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('mpl_toolkits', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('mpl_toolkits', [os.path.dirname(p)])));m = m or sys.modules.setdefault('mpl_toolkits', types.ModuleType('mpl_toolkits'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
||||||
|
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('mpl_toolkits',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('mpl_toolkits', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('mpl_toolkits', [os.path.dirname(p)])));m = m or sys.modules.setdefault('mpl_toolkits', types.ModuleType('mpl_toolkits'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pip
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
Metadata-Version: 2.1
|
||||||
|
Name: matplotlib
|
||||||
|
Version: 3.0.2
|
||||||
|
Summary: Python plotting package
|
||||||
|
Home-page: http://matplotlib.org
|
||||||
|
Author: John D. Hunter, Michael Droettboom
|
||||||
|
Author-email: matplotlib-users@python.org
|
||||||
|
License: BSD
|
||||||
|
Download-URL: http://matplotlib.org/users/installing.html
|
||||||
|
Platform: any
|
||||||
|
Classifier: Development Status :: 5 - Production/Stable
|
||||||
|
Classifier: Intended Audience :: Science/Research
|
||||||
|
Classifier: License :: OSI Approved :: Python Software Foundation License
|
||||||
|
Classifier: Programming Language :: Python
|
||||||
|
Classifier: Programming Language :: Python :: 3
|
||||||
|
Classifier: Programming Language :: Python :: 3.5
|
||||||
|
Classifier: Programming Language :: Python :: 3.6
|
||||||
|
Classifier: Programming Language :: Python :: 3.7
|
||||||
|
Classifier: Topic :: Scientific/Engineering :: Visualization
|
||||||
|
Requires-Python: >=3.5
|
||||||
|
Requires-Dist: numpy (>=1.10.0)
|
||||||
|
Requires-Dist: cycler (>=0.10)
|
||||||
|
Requires-Dist: kiwisolver (>=1.0.1)
|
||||||
|
Requires-Dist: pyparsing (!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1)
|
||||||
|
Requires-Dist: python-dateutil (>=2.1)
|
||||||
|
|
||||||
|
|
||||||
|
Matplotlib strives to produce publication quality 2D graphics
|
||||||
|
for interactive graphing, scientific publishing, user interface
|
||||||
|
development and web application servers targeting multiple user
|
||||||
|
interfaces and hardcopy output formats.
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,919 @@
|
|||||||
|
__pycache__/pylab.cpython-36.pyc,,
|
||||||
|
matplotlib-3.0.2-py3.6-nspkg.pth,sha256=HBCg6BgtP04BPqHtaNyC13Cbp9tWa8jd68ltGEV1XF8,1138
|
||||||
|
matplotlib-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||||
|
matplotlib-3.0.2.dist-info/METADATA,sha256=fs4NGrFCD56DFwQffsSExcU818TFY8T9OTinV1dNP5w,1226
|
||||||
|
matplotlib-3.0.2.dist-info/RECORD,,
|
||||||
|
matplotlib-3.0.2.dist-info/WHEEL,sha256=d2ILPScH-y2UwGxsW1PeA2TT-KW0Git4AJ6LeOK8sQo,109
|
||||||
|
matplotlib-3.0.2.dist-info/namespace_packages.txt,sha256=LQMWCv385LtvVcrCFmS8Kk1axcWAaUG9ycvuMhV6yoA,26
|
||||||
|
matplotlib-3.0.2.dist-info/top_level.txt,sha256=9tEw2ni8DdgX8CceoYHqSH1s50vrJ9SDfgtLIG8e3Y4,30
|
||||||
|
matplotlib/.libs/libpng16-cfdb1654.so.16.21.0,sha256=Fo8LBDWTuCclLkpSng_KP5pI7wcQtuXA9opT1FFkXl0,275648
|
||||||
|
matplotlib/.libs/libz-a147dcb0.so.1.2.3,sha256=1IGoOjRpujOMRn7cZ29ERtAxBt6SxTUlRLBkSqa_lsk,87848
|
||||||
|
matplotlib/__init__.py,sha256=pWjrFCjP1acCOKaKs7AMHt1xVBiNQ_EXQtq6DUJJQUU,64067
|
||||||
|
matplotlib/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_animation_data.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_cm.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_cm_listed.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_color_data.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_constrained_layout.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_layoutbox.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_mathtext_data.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_pylab_helpers.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/_version.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/afm.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/animation.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/artist.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/axis.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/backend_bases.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/backend_managers.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/backend_tools.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/bezier.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/blocking_input.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/category.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/cm.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/collections.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/colorbar.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/colors.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/container.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/contour.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/dates.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/docstring.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/dviread.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/figure.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/font_manager.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/fontconfig_pattern.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/gridspec.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/hatch.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/image.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/legend.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/legend_handler.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/lines.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/markers.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/mathtext.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/mlab.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/offsetbox.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/patches.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/path.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/patheffects.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/pylab.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/pyplot.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/quiver.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/rcsetup.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/sankey.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/scale.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/spines.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/stackplot.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/streamplot.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/table.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/texmanager.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/text.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/textpath.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/ticker.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/tight_bbox.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/tight_layout.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/transforms.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/type1font.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/units.cpython-36.pyc,,
|
||||||
|
matplotlib/__pycache__/widgets.cpython-36.pyc,,
|
||||||
|
matplotlib/_animation_data.py,sha256=CCdf8YwNX_08FS3YFPYzr2wi3id_WXIKLHrqM50HM8g,6157
|
||||||
|
matplotlib/_cm.py,sha256=C3xi_H7o8WF6qSv3Jl0DA1T2vbT-4wIDLFYfuJe5Zpg,66609
|
||||||
|
matplotlib/_cm_listed.py,sha256=EpTjQ6pZ9E_UeY4kDa6fU9za_VHBvhuOmCG6fwNRvlk,98362
|
||||||
|
matplotlib/_color_data.py,sha256=9hUzbyqLpEe-2LjEeAN3ja2ANuryNvtTseU8vuJXfsI,34776
|
||||||
|
matplotlib/_constrained_layout.py,sha256=1ygKBGfY0ttpPFhEWucL0wVnwYP-U_ZX8OxHlCUXXjQ,29304
|
||||||
|
matplotlib/_contour.cpython-36m-x86_64-linux-gnu.so,sha256=Gd6D_VYbA-imr5k186kroDG91DrOUYiltJS3QuARruQ,95144
|
||||||
|
matplotlib/_image.cpython-36m-x86_64-linux-gnu.so,sha256=akH-urj3-vOY9iYC-N9lORYlpSVVMPUKMBnCAVPrqc0,242496
|
||||||
|
matplotlib/_layoutbox.py,sha256=yXrJA2hBYKdzPNzbrsLl0Lv8zAPtnPulQ9-BbfhcVWM,24354
|
||||||
|
matplotlib/_mathtext_data.py,sha256=CmKFRW6mXCJqgZSQaiNOSG_VUn9WiSx5Hrg-4qKIn14,89371
|
||||||
|
matplotlib/_path.cpython-36m-x86_64-linux-gnu.so,sha256=jEhQzvD031IYQRSbo4Fxobi5KpZF-NV_7iuDDRaVBDU,190216
|
||||||
|
matplotlib/_png.cpython-36m-x86_64-linux-gnu.so,sha256=DlT39C99TemBT-i-7MuJuwJRg6eVwRhMMR4AxInU2QI,48144
|
||||||
|
matplotlib/_pylab_helpers.py,sha256=lC5IY7NOGCPVD5brJj-PdtJkzA53Fs4hrn9exdzWDFg,3528
|
||||||
|
matplotlib/_qhull.cpython-36m-x86_64-linux-gnu.so,sha256=dpM763p27zLpJIW8u045wQW71Oz3JwP9BHCHW6M-pHY,382640
|
||||||
|
matplotlib/_tri.cpython-36m-x86_64-linux-gnu.so,sha256=7InLl8WYUN4CyAtbrAWCXzycxTUs7_xNZx4QZKcifZg,128616
|
||||||
|
matplotlib/_version.py,sha256=bHCJPWh4TT1eI2_VgNQWyLUAJMklrvz_FkivkMGVXmk,471
|
||||||
|
matplotlib/afm.py,sha256=Cfj2v5Rgsr5GaDzuKySLd-aqsBdJBLX2T1gt7TQ175o,16934
|
||||||
|
matplotlib/animation.py,sha256=WyFTZBvyIN2tmvnT9LzlGZDOvqWgPn0SHOfzl0e1LlY,67680
|
||||||
|
matplotlib/artist.py,sha256=VbAnAHa4CPr39s_99S0yAD9-tBUMPxc-BLvhWeykSLk,48885
|
||||||
|
matplotlib/axes/__init__.py,sha256=npQuBvs_xEBEGUP2-BBZzCrelsAQYgB1U96kSZTSWIs,46
|
||||||
|
matplotlib/axes/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/axes/__pycache__/_axes.cpython-36.pyc,,
|
||||||
|
matplotlib/axes/__pycache__/_base.cpython-36.pyc,,
|
||||||
|
matplotlib/axes/__pycache__/_subplots.cpython-36.pyc,,
|
||||||
|
matplotlib/axes/_axes.py,sha256=o_hBMnccGhT_BedP7n12Kk6Y-qhEmL7S3guI5EF7r_M,306901
|
||||||
|
matplotlib/axes/_base.py,sha256=-gd9r0IAp785gRHpHpaN1IibgC5J069NNPKH2d_UbHQ,156195
|
||||||
|
matplotlib/axes/_subplots.py,sha256=pPG7GjDBFGDnjVXdojw3_ROHUO5kjZM2s7FsUwIH7sE,9479
|
||||||
|
matplotlib/axis.py,sha256=s2rcQiyNOCskQ3e4J9YeG-dI0GMbH8v3SWbrC7ymtPo,90811
|
||||||
|
matplotlib/backend_bases.py,sha256=7EaSuF58KKjm5sRYnmsUECkSJc0BcbsYmGd3gMpcpJM,111487
|
||||||
|
matplotlib/backend_managers.py,sha256=R3ZGS7NiyfAYutPX9KRqRXiZur8VVFb2_yQPNKtQ2Os,12983
|
||||||
|
matplotlib/backend_tools.py,sha256=nCruryzfTxOT0v8wyQPzPvCuNdOx5eWXTiiVniGoQmg,35810
|
||||||
|
matplotlib/backends/__init__.py,sha256=Y2fIYrWuxw3DsTGBgaYTkoExgiv5V-fpFMoL6Xluv1U,3593
|
||||||
|
matplotlib/backends/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/_backend_tk.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/_gtk3_compat.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_agg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_cairo.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_gtk3.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_gtk3agg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_gtk3cairo.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_macosx.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_mixed.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_nbagg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_pdf.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_pgf.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_ps.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_qt4.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_qt4agg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_qt4cairo.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_qt5.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_qt5agg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_qt5cairo.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_svg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_template.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_tkagg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_tkcairo.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_webagg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_webagg_core.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_wx.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_wxagg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/backend_wxcairo.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/qt_compat.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/tkagg.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/windowing.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/__pycache__/wx_compat.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/_backend_agg.cpython-36m-x86_64-linux-gnu.so,sha256=7egU7A_XSGu5zAhXxcs7ubTrUwDx9wCah09ET3u9BTg,358384
|
||||||
|
matplotlib/backends/_backend_tk.py,sha256=oG6iWxbo96fmE24Q1-2p62MX2sSXwp-bDsCiteqw7bw,37878
|
||||||
|
matplotlib/backends/_gtk3_compat.py,sha256=_mQjgaLq7PAdWCmkYkN3ZEc7SFms4T47Y6X1bmc3UlI,1458
|
||||||
|
matplotlib/backends/_tkagg.cpython-36m-x86_64-linux-gnu.so,sha256=ghIFRXt4_3htN74rENcBbpBc_Ly2pRg94F6h_xxE7vI,29080
|
||||||
|
matplotlib/backends/backend_agg.py,sha256=oWgXuSgkOr_w_Ix3xB07QwaW34CIrDeFr3Ar38KWLnI,21136
|
||||||
|
matplotlib/backends/backend_cairo.py,sha256=F_Er53Cssgiaz22qKUExxLvgBssxmsl0vRTenkckDJA,23049
|
||||||
|
matplotlib/backends/backend_gtk3.py,sha256=7yldMkTpLnLqd9GLi905Ba2Se0gBe2bNNjrojNYDKMM,34061
|
||||||
|
matplotlib/backends/backend_gtk3agg.py,sha256=Sdv_sVpJab05fFqZEz4a8hv2jB6194l0S7I9kioDCBc,2863
|
||||||
|
matplotlib/backends/backend_gtk3cairo.py,sha256=qvEc-eOfNhYhvG_4COnmCt3e_L-dInGMMnWJY3EG1s8,1516
|
||||||
|
matplotlib/backends/backend_macosx.py,sha256=cQLOUeHfteu3UzK5VhdBwNRohFidcNGsJ9NNCpcx52Y,6534
|
||||||
|
matplotlib/backends/backend_mixed.py,sha256=w0npmW09OKYhaHlpaKXY9vBhKNpUrL_gplxhjlm_re0,5738
|
||||||
|
matplotlib/backends/backend_nbagg.py,sha256=981WrP82fEZJIdND77C2JW4DpS3k82J6PyXthy18-jM,8707
|
||||||
|
matplotlib/backends/backend_pdf.py,sha256=L6GwqJKiPaFlqPRuhFbJQ9xGhZf8DGkL-FQ5W_nSamw,96965
|
||||||
|
matplotlib/backends/backend_pgf.py,sha256=8a9VSoU32ZatMBik36IjmrM56GkU0QFc3w1o3z3y89w,43189
|
||||||
|
matplotlib/backends/backend_ps.py,sha256=M47hHy6DgqRev1emHxv-K8diIoP5aYsNKW1sMex2h2A,61117
|
||||||
|
matplotlib/backends/backend_qt4.py,sha256=x9KHRXMxJfmlJ-6lMdmKrw9cGe5RlZAAGjk1Ga1xX2c,410
|
||||||
|
matplotlib/backends/backend_qt4agg.py,sha256=xTZMUL161hqcsOKSeU4sEs9kYloQrC7_OJjfyuYUAp0,245
|
||||||
|
matplotlib/backends/backend_qt4cairo.py,sha256=B-LD_AECxVVsZA6Zb-oxoN39iM-GUNl81LR7nVaJ8q4,159
|
||||||
|
matplotlib/backends/backend_qt5.py,sha256=HTxAS2-cOPD_pS40BP4foUl-UfQb3XHmQrNJsuWE8Vs,41475
|
||||||
|
matplotlib/backends/backend_qt5agg.py,sha256=plgjHJqJ0IawKuEH8Re_mTOLLrH90mRcMs65nbdTLvE,3237
|
||||||
|
matplotlib/backends/backend_qt5cairo.py,sha256=wTVbxWW9f5REOesXDIzI2eSmE5HS-nwWNcg2m-YvyLI,1977
|
||||||
|
matplotlib/backends/backend_svg.py,sha256=I_-tR0sEpRFt7jlM8S-dJX8vgjwMifEJTa5N3maJWC0,45518
|
||||||
|
matplotlib/backends/backend_template.py,sha256=drJi5J7r4ZMpmwvYiVp3uVfWDJ0VtZSEd2A4OD93fUU,9149
|
||||||
|
matplotlib/backends/backend_tkagg.py,sha256=WMslLWYmtxlmAaBH4tx4HjmRDWMKiSV91KHF9yeMRng,676
|
||||||
|
matplotlib/backends/backend_tkcairo.py,sha256=dVCh7ZD_2OR0DBQ0N3icD8cDV1SeEzCsRja446wWhPw,1069
|
||||||
|
matplotlib/backends/backend_webagg.py,sha256=kqDsqw1a0mZA3-ouBGegg8BlWys5iAA0-fAtxTvUZ5o,11124
|
||||||
|
matplotlib/backends/backend_webagg_core.py,sha256=c8FqOMph-FdYJ-kNauNG23enXWwZwbmQ3QzjhYud4IM,17554
|
||||||
|
matplotlib/backends/backend_wx.py,sha256=gQTIV29utVqq_W6iqRE5gqGwhDZ3sPg2s0fxXDkmcCY,74559
|
||||||
|
matplotlib/backends/backend_wxagg.py,sha256=TGdhDULgRzqhLNOzq7QAt0oe_AqHoT-IyZOO8RlE4Bw,4015
|
||||||
|
matplotlib/backends/backend_wxcairo.py,sha256=VC5TyJaX8TPLSgHv5ckAreoGrY_KiNRMQjVInMLlcFk,1843
|
||||||
|
matplotlib/backends/qt_compat.py,sha256=gdDU16Oe1HfwJJSGmvLvEJmrKDX6T8Tt1WiSUITR3qU,6658
|
||||||
|
matplotlib/backends/qt_editor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
matplotlib/backends/qt_editor/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/qt_editor/__pycache__/figureoptions.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/qt_editor/__pycache__/formlayout.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/qt_editor/__pycache__/formsubplottool.cpython-36.pyc,,
|
||||||
|
matplotlib/backends/qt_editor/figureoptions.py,sha256=_TD7NJY_LzXDoljckLqKaVBYa3FSJwEFNrccYpbKMTM,9133
|
||||||
|
matplotlib/backends/qt_editor/formlayout.py,sha256=qCAmYnZUEwgBRMVsPWuu7e1fqCU0OSM-I5Snw7ShcBI,19573
|
||||||
|
matplotlib/backends/qt_editor/formsubplottool.py,sha256=HiiXkwCotra_hI9JU208KOs8Q9JuGH1uAW3mV5l3Evg,1934
|
||||||
|
matplotlib/backends/tkagg.py,sha256=ro4lL5U0cLMslYmLXQRhWwR1mxlcZYGO9awistZzL1E,1319
|
||||||
|
matplotlib/backends/web_backend/all_figures.html,sha256=GiIHkdjLO94c_GAHVX4Zk5R88uEnIUwW2CJgm3qCCv0,1512
|
||||||
|
matplotlib/backends/web_backend/css/boilerplate.css,sha256=qui16QXRnQFNJDbcMasfH6KtN9hLjv8883U9cJmsVCE,2310
|
||||||
|
matplotlib/backends/web_backend/css/fbm.css,sha256=Us0osu_rK8EUAdp_GXrh89tN_hUNCN-r7N1T1NvmmwI,1473
|
||||||
|
matplotlib/backends/web_backend/css/page.css,sha256=Djf6ZNMFaM6_hVaizSkDFoqk-jn81qgduwles4AroGk,1599
|
||||||
|
matplotlib/backends/web_backend/ipython_inline_figure.html,sha256=mzi-yWg4fcO6PdtTBCfiNuvcv04T53lcRQi-8hphwuE,1305
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_diagonals-thick_18_b81900_40x40.png,sha256=xRM8xoz-7ahUtdsImL_ZC6s8kJT2QnE04-cji0ZQfsE,418
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_diagonals-thick_20_666666_40x40.png,sha256=T9PQekCQeDFHwAxdZMQs_QzAqaqtimo0uLCotJ0xSqg,312
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_flat_10_000000_40x100.png,sha256=r1CSqnKXLAw-qLRMm5LbIAnqQWB3OLzAqdpZesaoc-o,205
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_glass_100_f6f6f6_1x400.png,sha256=_ws_N6fYaeQspm2VHS5Wm-QV4CXhlUoSqGQ-lmW3xm0,262
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_glass_100_fdf5ce_1x400.png,sha256=-VfVTyNvfByi7t0zq33BBb1mLO5iuIBvsso4Ye2ysXw,348
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_glass_65_ffffff_1x400.png,sha256=JFJf1ebnJmze7uXUNtoLcgn-dTh-bJxVxXVLIuQDGrU,207
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_gloss-wave_35_f6a828_500x100.png,sha256=Av4RPyWlnMm1at-9VqlutTAvTKW7637sDlQamg2ozNA,5815
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_highlight-soft_100_eeeeee_1x100.png,sha256=80rH2tcJybpprH1zkHIN1U_aVhUcZOc9mv9OEYavhRA,278
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_highlight-soft_75_ffe45c_1x100.png,sha256=JRY-0684rYlKOQKRQmXYJ5YiPhHS3kNPZmUVa3xi3hg,328
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_222222_256x240.png,sha256=_htyYBLdV3XU9kp9QnMKIQ8pBX6OgU8zkE05EsTZq9s,6922
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_228ef1_256x240.png,sha256=Z8eq2yeIM45jXe3uMbiKqOqQjA2MKR20tWcBTVc6bC8,4549
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_ef8c08_256x240.png,sha256=loEi-IH5oyL8PSSjAGPT549tcpGfRzIbFEaqPFhR8ck,4549
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_ffd27a_256x240.png,sha256=O-dEGxrQchuZxTdpQJQ4LEirQLT1OA5lBNYO9O8mJo8,4549
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_ffffff_256x240.png,sha256=sVicDAn7JIIW-osNHhgSqa-bSRWOtS4G94BitP_MAds,6299
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/jquery-ui.css,sha256=zs9cWf98KIv5DMYiF1a9lhJGQwhVe5LKVPJ9HNEI880,35348
|
||||||
|
matplotlib/backends/web_backend/jquery/css/themes/base/jquery-ui.min.css,sha256=VQzrlVm7QjdSeQn_IecZgE9rnfM390H3VoIcDJljOSs,30163
|
||||||
|
matplotlib/backends/web_backend/jquery/js/jquery-1.11.3.js,sha256=kaIBSZlozqJ4IeqPwOlDOi97qhNHWTchpUedwo5-gMU,284395
|
||||||
|
matplotlib/backends/web_backend/jquery/js/jquery-1.11.3.min.js,sha256=7LkWEzqTdpEfELxcZZlS6wAx5Ff13zZ83lYO2_ujj7g,95957
|
||||||
|
matplotlib/backends/web_backend/jquery/js/jquery-ui.js,sha256=DI6NdAhhFRnO2k51mumYeDShet3I8AKCQf_tf7ARNhI,470596
|
||||||
|
matplotlib/backends/web_backend/jquery/js/jquery-ui.min.js,sha256=xNjb53_rY-WmG-4L6tTl9m6PpqknWZvRt0rO1SRnJzw,240427
|
||||||
|
matplotlib/backends/web_backend/js/mpl.js,sha256=SqYBZRsHYoQY9d-5eCZsVpIBnlR4T7t1JiYYSZOs6Uw,16964
|
||||||
|
matplotlib/backends/web_backend/js/mpl_tornado.js,sha256=lSxC7-yqF1GYY-6SheaHanx6SujMdcG7Vx2_3qbi-9Q,272
|
||||||
|
matplotlib/backends/web_backend/js/nbagg_mpl.js,sha256=WfV96-6LXzUSnzCmvQ93JopqO7KPqnfbT_07Q1XJeT4,7467
|
||||||
|
matplotlib/backends/web_backend/nbagg_uat.ipynb,sha256=y1N8hQzBJ05rJ2hZla2_Mw6tOUfNP1UHKo636W1e098,15933
|
||||||
|
matplotlib/backends/web_backend/single_figure.html,sha256=56UAOD6qgZ-T6wE2EjGpTBG4iwAmfG3QsnX5lLQp2dI,1203
|
||||||
|
matplotlib/backends/windowing.py,sha256=q-hSo_vvsjst6EjNkr94NIDbUucMocrMj9MSjVwkfY4,822
|
||||||
|
matplotlib/backends/wx_compat.py,sha256=RQFVDMaMLjT8YmazLiq3BcjCcCi6xuzS_AwmxtYd2vg,968
|
||||||
|
matplotlib/bezier.py,sha256=zwC07cDX9iyh9KWnZsPBQaFJ4CTue0NQpm1VwS2PEuM,15415
|
||||||
|
matplotlib/blocking_input.py,sha256=E6r1m5acvx_KLDp7rZXftnA5OBtJnjy_zGWBUO9k_OA,11112
|
||||||
|
matplotlib/category.py,sha256=AWYm1nucRySTh-6cXN2FeNYQOpwJPeUl9iFkjcr4oW0,5928
|
||||||
|
matplotlib/cbook/__init__.py,sha256=bFrhSeVf8Ya6pNEtkqOTVABSR--ADZLbTlsLqd3BUV4,65667
|
||||||
|
matplotlib/cbook/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/cbook/__pycache__/deprecation.cpython-36.pyc,,
|
||||||
|
matplotlib/cbook/deprecation.py,sha256=5ghVk2E7syukLQuADs7UtHuBRJYaiuvylSymAh_axDQ,9402
|
||||||
|
matplotlib/cm.py,sha256=MHQtLJ9UGDuyxHa-qyMxL2vikkhXB5OXhYdxUy-_68M,12843
|
||||||
|
matplotlib/collections.py,sha256=HqR0G7ydei6YkX06MYwp0TGrVZaXz7BDNiwEPGk0Fro,66851
|
||||||
|
matplotlib/colorbar.py,sha256=_UImtSk1sRHbLGNn_gLTy86mFYqzjovJMvioqcvn57w,59164
|
||||||
|
matplotlib/colors.py,sha256=HeZGu5MwSkMLEABhU5qYmJw4ZbvpxeOrdV2ytY_klSg,71636
|
||||||
|
matplotlib/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
matplotlib/compat/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/compat/__pycache__/subprocess.cpython-36.pyc,,
|
||||||
|
matplotlib/compat/subprocess.py,sha256=-K36IW-t5SjjWcP4nL80GAE-y7v0HZE6nQ-hscwmkvg,1562
|
||||||
|
matplotlib/container.py,sha256=Sbz7JTBAFirjClv-nSpVojuSLKIBoUo-dAve8QAFjl8,5245
|
||||||
|
matplotlib/contour.py,sha256=XiEg_unL8WQaQf3f3c41KBHltZPXJ8Fhk2W0OGvuacc,70231
|
||||||
|
matplotlib/dates.py,sha256=XsBhin-wZcY7FPcMrDQDBEYjYXNtmc-YbtwZiysKeqQ,61547
|
||||||
|
matplotlib/docstring.py,sha256=M5wraMzJf7veSfzUJUArSY68bM5lkH3q6niSmW0q1xw,3787
|
||||||
|
matplotlib/dviread.py,sha256=s_N3IU68PIVm03TFnBP8_UakGnIYwYCv1vxLDjpWsk4,37282
|
||||||
|
matplotlib/figure.py,sha256=p9yke2KjrUkOJ5ZUt-q44GqMjCU5woMnFMAXLR3-DJk,95506
|
||||||
|
matplotlib/font_manager.py,sha256=uWPJ8AR9gzeIljZz8baIRTDacgsAMORLgmV1lT6Ve_s,45972
|
||||||
|
matplotlib/fontconfig_pattern.py,sha256=LYKBekGIwiHNYyYe36Lfki4JyEkZMsBinmhVzBc1wns,6593
|
||||||
|
matplotlib/ft2font.cpython-36m-x86_64-linux-gnu.so,sha256=xKwvjhkVt1lLcRA4Swcz9iHqhWTk78UGl6CLyR5tgvw,894456
|
||||||
|
matplotlib/gridspec.py,sha256=C5rAu1tokWgaegWBEocG6phEfemgMF9dch-HwVfKC0A,20056
|
||||||
|
matplotlib/hatch.py,sha256=kmq09LW_AJxPY0LGJxJrrC9Yf6tPQYi209EMAveqQoE,6988
|
||||||
|
matplotlib/image.py,sha256=LGgYY2UmfjldiGhqAU6Qwi-3I_CJAzbL6b9hFMhCWHQ,54632
|
||||||
|
matplotlib/legend.py,sha256=M6PCguGj32SpnEuLvPhRKfWZuCofn1JYSubhRb9e5Yk,48770
|
||||||
|
matplotlib/legend_handler.py,sha256=gmQCUHjMmvcJ4g8dSqayhaXj82jkUK36i0-4DK6ik-A,25524
|
||||||
|
matplotlib/lines.py,sha256=uKymslFnSniquZR_5N266mXvkQJ_4eKu_sg5iP5t1rA,48503
|
||||||
|
matplotlib/markers.py,sha256=yILg87-gXelLwbu2o1xHNdCikXVHG50frJQJ6ywY8OY,34345
|
||||||
|
matplotlib/mathtext.py,sha256=vXlAVF9T7xA7-uRHnBu4T6LBZhr97a6obWEAr-X-mA4,121307
|
||||||
|
matplotlib/mlab.py,sha256=mhkOI6_Wgw0aaaqPDuCE53tHLNxl8tj_ajgoCZbRUcY,124056
|
||||||
|
matplotlib/mpl-data/fonts/afm/cmex10.afm,sha256=blR3ERmrVBV5XKkAnDCj4NMeYVgzH7cXtJ3u59u9GuE,12070
|
||||||
|
matplotlib/mpl-data/fonts/afm/cmmi10.afm,sha256=5qwEOpedEo76bDUahyuuF1q0cD84tRrX-VQ4p3MlfBo,10416
|
||||||
|
matplotlib/mpl-data/fonts/afm/cmr10.afm,sha256=WDvgC_D3UkGJg9u-J0U6RaT02lF4oz3lQxHtg1r3lYw,10101
|
||||||
|
matplotlib/mpl-data/fonts/afm/cmsy10.afm,sha256=AbmzvCVWBceHRfmRfeJ9E6xzOQTFLk0U1zDfpf3_MaM,8295
|
||||||
|
matplotlib/mpl-data/fonts/afm/cmtt10.afm,sha256=4ji7_mTpeWMa93o_UHBWPKCnqsBfhJJNllat1lJArP4,6501
|
||||||
|
matplotlib/mpl-data/fonts/afm/pagd8a.afm,sha256=jjFrigwkTpYLqa26cpzZvKQNBo-PuF4bmDVqaM4pMWw,17183
|
||||||
|
matplotlib/mpl-data/fonts/afm/pagdo8a.afm,sha256=sgNQdeYyx8J-itGw9h31y95aMBiTCRvmNSPTXwwS7xg,17255
|
||||||
|
matplotlib/mpl-data/fonts/afm/pagk8a.afm,sha256=ZUtfHPloNqcvGMHMxaKDSlshhOcjwheUx143RwpGdIU,17241
|
||||||
|
matplotlib/mpl-data/fonts/afm/pagko8a.afm,sha256=Yj1wBg6Jsqqz1KBfhRoJ3ACR-CMQol8Fj_ZM5NZ1gDk,17346
|
||||||
|
matplotlib/mpl-data/fonts/afm/pbkd8a.afm,sha256=Zl5o6J_di9Y5j2EpHtjew-_sfg7-WoeVmO9PzOYSTUc,15157
|
||||||
|
matplotlib/mpl-data/fonts/afm/pbkdi8a.afm,sha256=JAOno930iTyfZILMf11vWtiaTgrJcPpP6FRTRhEMMD4,15278
|
||||||
|
matplotlib/mpl-data/fonts/afm/pbkl8a.afm,sha256=UJqJjOJ6xQDgDBLX157mKpohIJFVmHM-N6x2-DiGv14,15000
|
||||||
|
matplotlib/mpl-data/fonts/afm/pbkli8a.afm,sha256=AWislZ2hDbs0ox_qOWREugsbS8_8lpL48LPMR40qpi0,15181
|
||||||
|
matplotlib/mpl-data/fonts/afm/pcrb8a.afm,sha256=6j1TS2Uc7DWSc-8l42TGDc1u0Fg8JspeWfxFayjUwi8,15352
|
||||||
|
matplotlib/mpl-data/fonts/afm/pcrbo8a.afm,sha256=smg3mjl9QaBDtQIt06ko5GvaxLsO9QtTvYANuE5hfG0,15422
|
||||||
|
matplotlib/mpl-data/fonts/afm/pcrr8a.afm,sha256=7nxFr0Ehz4E5KG_zSE5SZOhxRH8MyfnCbw-7x5wu7tw,15339
|
||||||
|
matplotlib/mpl-data/fonts/afm/pcrro8a.afm,sha256=NKEz7XtdFkh9cA8MvY-S3UOZlV2Y_J3tMEWFFxj7QSg,15443
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvb8a.afm,sha256=NAx4M4HjL7vANCJbc-tk04Vkol-T0oaXeQ3T2h-XUvM,17155
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvb8an.afm,sha256=8e_myD-AQkNF7q9XNLb2m76_lX2TUr3a5wog_LIE1sk,17086
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvbo8a.afm,sha256=8fkBRmJ-SWY2YrBg8fFyjJyrJp8daQ6JPO6LvhM8xPI,17230
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvbo8an.afm,sha256=aeVRvV4r15BBvxuRJ0MG8ZHuH2HViuIiCYkvuapmkmM,17195
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvl8a.afm,sha256=IyMYM-bgl-gI6rG0EuZZ2OLzlxJfGeSh8xqsh0t-eJQ,15627
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvlo8a.afm,sha256=s12C-eNnIDHJ_UVbuiprjxBjCiHIbS3Y8ORTC-qTpuI,15729
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvr8a.afm,sha256=Kt8KaRidts89EBIK29X2JomDUEDxvroeaJz_RNTi6r4,17839
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvr8an.afm,sha256=lL5fAHTRwODl-sB5mH7IfsD1tnnea4yRUK-_Ca2bQHM,17781
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvro8a.afm,sha256=3KqK3eejiR4hIFBUynuSX_4lMdE2V2T58xOF8lX-fwc,17919
|
||||||
|
matplotlib/mpl-data/fonts/afm/phvro8an.afm,sha256=Vx9rRf3YfasMY7tz-njSxz67xHKk-fNkN7yBi0X2IP0,17877
|
||||||
|
matplotlib/mpl-data/fonts/afm/pncb8a.afm,sha256=aoXepTcDQtQa_mspflMJkEFKefzXHoyjz6ioJVI0YNc,16028
|
||||||
|
matplotlib/mpl-data/fonts/afm/pncbi8a.afm,sha256=pCWW1MYgy0EmvwaYsaYJaAI_LfrsKmDANHu7Pk0RaiU,17496
|
||||||
|
matplotlib/mpl-data/fonts/afm/pncr8a.afm,sha256=0CIB2BLe9r-6_Wl5ObRTTf98UOrezmGQ8ZOuBX5kLks,16665
|
||||||
|
matplotlib/mpl-data/fonts/afm/pncri8a.afm,sha256=5R-pLZOnaHNG8pjV6MP3Ai-d2OTQYR_cYCb5zQhzfSU,16920
|
||||||
|
matplotlib/mpl-data/fonts/afm/pplb8a.afm,sha256=3EzUbNnXr5Ft5eFLY00W9oWu59rHORgDXUuJaOoKN58,15662
|
||||||
|
matplotlib/mpl-data/fonts/afm/pplbi8a.afm,sha256=X_9tVspvrcMer3OS8qvdwjFFqpAXYZneyCL2NHA902g,15810
|
||||||
|
matplotlib/mpl-data/fonts/afm/pplr8a.afm,sha256=ijMb497FDJ9nVdVMb21F7W3-cu9sb_9nF0oriFpSn8k,15752
|
||||||
|
matplotlib/mpl-data/fonts/afm/pplri8a.afm,sha256=8KITbarcUUMi_hdoRLLmNHtlqs0TtOSKqtPFft7X5nY,15733
|
||||||
|
matplotlib/mpl-data/fonts/afm/psyr.afm,sha256=Iyt8ajE4B2Tm34oBj2pKtctIf9kPfq05suQefq8p3Ro,9644
|
||||||
|
matplotlib/mpl-data/fonts/afm/ptmb8a.afm,sha256=bL1fA1NC4_nW14Zrnxz4nHlXJb4dzELJPvodqKnYeMg,17983
|
||||||
|
matplotlib/mpl-data/fonts/afm/ptmbi8a.afm,sha256=-_Ui6XlKaFTHEnkoS_-1GtIr5VtGa3gFQ2ezLOYHs08,18070
|
||||||
|
matplotlib/mpl-data/fonts/afm/ptmr8a.afm,sha256=IEcsWcmzJyjCwkgsw4o6hIMmzlyXUglJat9s1PZNnEU,17942
|
||||||
|
matplotlib/mpl-data/fonts/afm/ptmri8a.afm,sha256=49fQMg5fIGguZ7rgc_2styMK55Pv5bPTs7wCzqpcGpk,18068
|
||||||
|
matplotlib/mpl-data/fonts/afm/putb8a.afm,sha256=qMaHTdpkrNL-m4DWhjpxJCSmgYkCv1qIzLlFfM0rl40,21532
|
||||||
|
matplotlib/mpl-data/fonts/afm/putbi8a.afm,sha256=g7AVJyiTxeMpNk_1cSfmYgM09uNUfPlZyWGv3D1vcAk,21931
|
||||||
|
matplotlib/mpl-data/fonts/afm/putr8a.afm,sha256=XYmNC5GQgSVAZKTIYdYeNksE6znNm9GF_0SmQlriqx0,22148
|
||||||
|
matplotlib/mpl-data/fonts/afm/putri8a.afm,sha256=i7fVe-iLyLtQxCfAa4IxdxH-ufcHmMk7hbCGG5TxAY4,21891
|
||||||
|
matplotlib/mpl-data/fonts/afm/pzcmi8a.afm,sha256=wyuoIWEZOcoXrSl1tPzLkEahik7kGi91JJj-tkFRG4A,16250
|
||||||
|
matplotlib/mpl-data/fonts/afm/pzdr.afm,sha256=MyjLAnzKYRdQBfof1W3k_hf30MvqOkqL__G22mQ5xww,9467
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Courier-Bold.afm,sha256=sIDDI-B82VZ3C0mI_mHFITCZ7PVn37AIYMv1CrHX4sE,15333
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Courier-BoldOblique.afm,sha256=zg61QobD3YU9UBfCXmvmhBNaFKno-xj8sY0b2RpgfLw,15399
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Courier-Oblique.afm,sha256=vRQm5j1sTUN4hicT1PcVZ9P9DTTUHhEzfPXqUUzVZhE,15441
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Courier.afm,sha256=Mdcq2teZEBJrIqVXnsnhee7oZnTs6-P8_292kWGTrw4,15335
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica-Bold.afm,sha256=i2l4gcjuYXoXf28uK7yIVwuf0rnw6J7PwPVQeHj5iPw,69269
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica-BoldOblique.afm,sha256=Um5O6qK11DXLt8uj_0IoWkc84TKqHK3bObSKUswQqvY,69365
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica-Oblique.afm,sha256=hVYDg2b52kqtbVeCzmiv25bW1yYdpkZS-LXlGREN2Rs,74392
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica.afm,sha256=23cvKDD7bQAJB3kdjSahJSTZaUOppznlIO6FXGslyW8,74292
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Symbol.afm,sha256=P5UaoXr4y0qh4SiMa5uqijDT6ZDr2-jPmj1ayry593E,9740
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Times-Bold.afm,sha256=cQTmr2LFPwKQE_sGQageMcmFicjye16mKJslsJLHQyE,64251
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Times-BoldItalic.afm,sha256=pzWOdycm6RqocBWgAVY5Jq0z3Fp7LuqWgLNMx4q6OFw,59642
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Times-Italic.afm,sha256=bK5puSMpGT_YUILwyJrXoxjfj7XJOdfv5TQ_iKsJRzw,66328
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/Times-Roman.afm,sha256=hhNrUnpazuDDKD1WpraPxqPWCYLrO7D7bMVOg-zI13o,60460
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/ZapfDingbats.afm,sha256=ZuOmt9GcKofjdOq8kqhPhtAIhOwkL2rTJTmZxAjFakA,9527
|
||||||
|
matplotlib/mpl-data/fonts/pdfcorefonts/readme.txt,sha256=MRv8ppSITYYAb7lt5EOw9DWWNZIblfxsFhu5TQE7cpI,828
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSans-Bold.ttf,sha256=sYS4njwQdfIva3FXW2_CDUlys8_TsjMiym_Vltyu8Wc,704128
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSans-BoldOblique.ttf,sha256=bt8CgxYBhq9FHL7nHnuEXy5Mq_Jku5ks5mjIPCVGXm8,641720
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf,sha256=zN90s1DxH9PdV3TeUOXmNGoaXaH1t9X7g1kGZel6UhM,633840
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf,sha256=P99pyr8GBJ6nCgC1kZNA4s4ebQKwzDxLRPtoAb0eDSI,756072
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSansDisplay.ttf,sha256=ggmdz7paqGjN_CdFGYlSX-MpL3N_s8ngMozpzvWWUvY,25712
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Bold.ttf,sha256=uq2ppRcv4giGJRr_BDP8OEYZEtXa8HKH577lZiCo2pY,331536
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-BoldOblique.ttf,sha256=ppCBwVx2yCfgonpaf1x0thNchDSZlVSV_6jCDTqYKIs,253116
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf,sha256=KAUoE_enCfyJ9S0ZLcmV708P3Fw9e3OknWhJsZFtDNA,251472
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono.ttf,sha256=YC7Ia4lIz82VZIL-ZPlMNshndwFJ7y95HUYT9EO87LM,340240
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Bold.ttf,sha256=w3U_Lta8Zz8VhG3EWt2-s7nIcvMvsY_VOiHxvvHtdnY,355692
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSerif-BoldItalic.ttf,sha256=2T7-x6nS6CZ2jRou6VuVhw4V4pWZqE80hK8d4c7C4YE,347064
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Italic.ttf,sha256=PnmU-8VPoQzjNSpC1Uj63X2crbacsRCbydlg9trFfwQ,345612
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSerif.ttf,sha256=EHJElW6ZYrnpb6zNxVGCXgrgiYrhNzcTPhuSGi_TX_o,379740
|
||||||
|
matplotlib/mpl-data/fonts/ttf/DejaVuSerifDisplay.ttf,sha256=KRTzLkfHd8J75Wd6-ufbTeefnkXeb8kJfZlJwjwU99U,14300
|
||||||
|
matplotlib/mpl-data/fonts/ttf/LICENSE_DEJAVU,sha256=11k43sCY8G8Kw8AIUwZdlPAgvhw8Yu8dwpdboVtNmw4,4816
|
||||||
|
matplotlib/mpl-data/fonts/ttf/LICENSE_STIX,sha256=cxFOZdp1AxNhXR6XxCzf5iJpNcu-APm-geOHhD-s0h8,5475
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXGeneral.ttf,sha256=FnN4Ax4t3cYhbWeBnJJg6aBv_ExHjk4jy5im_USxg8I,448228
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf,sha256=6FM9xwg_o0a9oZM9YOpKg7Z9CUW86vGzVB-CtKDixqA,237360
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf,sha256=mHiP1LpI37sr0CbA4gokeosGxzcoeWKLemuw1bsJc2w,181152
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf,sha256=bPyzM9IrfDxiO9_UAXTxTIXD1nMcphZsHtyAFA6uhSc,175040
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXNonUni.ttf,sha256=Ulb34CEzWsSFTRgPDovxmJZOwvyCAXYnbhaqvGU3u1c,59108
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXNonUniBol.ttf,sha256=XRBqW3jR_8MBdFU0ObhiV7-kXwiBIMs7QVClHcT5tgs,30512
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXNonUniBolIta.ttf,sha256=pb22DnbDf2yQqizotc3wBDqFGC_g27YcCGJivH9-Le8,41272
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXNonUniIta.ttf,sha256=BMr9pWiBv2YIZdq04X4c3CgL6NPLUPrl64aV1N4w9Ug,46752
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizFiveSymReg.ttf,sha256=wYuH1gYUpCuusqItRH5kf9p_s6mUD-9X3L5RvRtKSxs,13656
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizFourSymBol.ttf,sha256=yNdvjUoSmsZCULmD7SVq9HabndG9P4dPhboL1JpAf0s,12228
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizFourSymReg.ttf,sha256=-9xVMYL4_1rcO8FiCKrCfR4PaSmKtA42ddLGqwtei1w,15972
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizOneSymBol.ttf,sha256=cYexyo8rZcdqMlpa9fNF5a2IoXLUTZuIvh0JD1Qp0i4,12556
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizOneSymReg.ttf,sha256=0lbHzpndzJmO8S42mlkhsz5NbvJLQCaH5Mcc7QZRDzc,19760
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymBol.ttf,sha256=3eBc-VtYbhQU3BnxiypfO6eAzEu8BdDvtIJSFbkS2oY,12192
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymReg.ttf,sha256=XFSKCptbESM8uxHtUFSAV2cybwxhSjd8dWVByq6f3w0,15836
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymBol.ttf,sha256=MUCYHrA0ZqFiSE_PjIGlJZgMuv79aUgQqE7Dtu3kuo0,12116
|
||||||
|
matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymReg.ttf,sha256=_sdxDuEwBDtADpu9CyIXQxV7sIqA2TZVBCUiUjq5UCk,15704
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmb10.ttf,sha256=B0SXtQxD6ldZcYFZH5iT04_BKofpUQT1ZX_CSB9hojo,25680
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmex10.ttf,sha256=ryjwwXByOsd2pxv6WVrKCemNFa5cPVTOGa_VYZyWqQU,21092
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmmi10.ttf,sha256=MJKWW4gR_WpnZXmWZIRRgfwd0TMLk3-RWAjEhdMWI00,32560
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmr10.ttf,sha256=Tdl2GwWMAJ25shRfVe5mF9CTwnPdPWxbPkP_YRD6m_Y,26348
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmss10.ttf,sha256=ffkag9BbLkcexjjLC0NaNgo8eSsJ_EKn2mfpHy55EVo,20376
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmsy10.ttf,sha256=uyJu2TLz8QDNDlL15JEu5VO0G2nnv9uNOFTbDrZgUjI,29396
|
||||||
|
matplotlib/mpl-data/fonts/ttf/cmtt10.ttf,sha256=YhHwmuk1mZka_alwwkZp2tGnfiU9kVYk-_IS9wLwcdc,28136
|
||||||
|
matplotlib/mpl-data/fonts/ttf/local.conf,sha256=wcg11V6oGPKn4c43Z_bGGo9TCyvjMWq9msuzMmdtRTo,988
|
||||||
|
matplotlib/mpl-data/images/back.gif,sha256=sdkxFRAh-Mgs44DTvruO5OxcI3Av9CS1g5MqMA_DDkQ,608
|
||||||
|
matplotlib/mpl-data/images/back.pdf,sha256=ZR7CJo_dAeCM-KlaGvskgtHQyRtrPIolc8REOmcoqJk,1623
|
||||||
|
matplotlib/mpl-data/images/back.png,sha256=E4dGf4Gnz1xJ1v2tMygHV0YNQgShreDeVApaMb-74mU,380
|
||||||
|
matplotlib/mpl-data/images/back.svg,sha256=yRdMiKsa-awUm2x_JE_rEV20rNTa7FInbFBEoMo-6ik,1512
|
||||||
|
matplotlib/mpl-data/images/back_large.gif,sha256=tqCtecrxNrPuDCUj7FGs8UXWftljKcwgp5cSBBhXwiQ,799
|
||||||
|
matplotlib/mpl-data/images/back_large.png,sha256=9A6hUSQeszhYONE4ZuH3kvOItM0JfDVu6tkfromCbsQ,620
|
||||||
|
matplotlib/mpl-data/images/filesave.gif,sha256=wAyNwOPd9c-EIPwcUAlqHSfLmxq167nhDVppOWPy9UA,723
|
||||||
|
matplotlib/mpl-data/images/filesave.pdf,sha256=P1EPPV2g50WTt8UaX-6kFoTZM1xVqo6S2H6FJ6Zd1ec,1734
|
||||||
|
matplotlib/mpl-data/images/filesave.png,sha256=b7ctucrM_F2mG-DycTedG_a_y4pHkx3F-zM7l18GLhk,458
|
||||||
|
matplotlib/mpl-data/images/filesave.svg,sha256=oxPVbLS9Pzelz71C1GCJWB34DZ0sx_pUVPRHBrCZrGs,2029
|
||||||
|
matplotlib/mpl-data/images/filesave_large.gif,sha256=IXrenlwu3wwO8WTRvxHt_q62NF6ZWyqk3jZhm6GE-G8,1498
|
||||||
|
matplotlib/mpl-data/images/filesave_large.png,sha256=LNbRD5KZ3Kf7nbp-stx_a1_6XfGBSWUfDdpgmnzoRvk,720
|
||||||
|
matplotlib/mpl-data/images/forward.gif,sha256=VNL9R-dECOX7wUAYPtU_DWn5hwi3SwLR17DhmBvUIxE,590
|
||||||
|
matplotlib/mpl-data/images/forward.pdf,sha256=KIqIL4YId43LkcOxV_TT5uvz1SP8k5iUNUeJmAElMV8,1630
|
||||||
|
matplotlib/mpl-data/images/forward.png,sha256=pKbLepgGiGeyY2TCBl8svjvm7Z4CS3iysFxcq4GR-wk,357
|
||||||
|
matplotlib/mpl-data/images/forward.svg,sha256=NnQDOenfjsn-o0aJMUfErrP320Zcx9XHZkLh0cjMHsk,1531
|
||||||
|
matplotlib/mpl-data/images/forward_large.gif,sha256=H6Jbcc7qJwHJAE294YqI5Bm-5irofX40cKRvYdrG_Ig,786
|
||||||
|
matplotlib/mpl-data/images/forward_large.png,sha256=36h7m7DZDHql6kkdpNPckyi2LKCe_xhhyavWARz_2kQ,593
|
||||||
|
matplotlib/mpl-data/images/hand.gif,sha256=3lRfmAqQU7A2t1YXXsB9IbwzK7FaRh-IZO84D5-xCrw,1267
|
||||||
|
matplotlib/mpl-data/images/hand.pdf,sha256=hspwkNY915KPD7AMWnVQs7LFPOtlcj0VUiLu76dMabQ,4172
|
||||||
|
matplotlib/mpl-data/images/hand.png,sha256=2cchRETGKa0hYNKUxnJABwkyYXEBPqJy_VqSPlT0W2Q,979
|
||||||
|
matplotlib/mpl-data/images/hand.svg,sha256=tsVIES_nINrAbH4FqdsCGOx0SVE37vcofSYBhnnaOP0,4888
|
||||||
|
matplotlib/mpl-data/images/hand_large.gif,sha256=H5IHmVTvOqHQb9FZ_7g7AlPt9gv-zRq0L5_Q9B7OuvU,973
|
||||||
|
matplotlib/mpl-data/images/help.pdf,sha256=CeE978IMi0YWznWKjIT1R8IrP4KhZ0S7usPUvreSgcA,1813
|
||||||
|
matplotlib/mpl-data/images/help.png,sha256=s4pQrqaQ0py8I7vc9hv3BI3DO_tky-7YBMpaHuBDCBY,472
|
||||||
|
matplotlib/mpl-data/images/help.ppm,sha256=mVPvgwcddzCM-nGZd8Lnl_CorzDkRIXQE17b7qo8vlU,1741
|
||||||
|
matplotlib/mpl-data/images/help.svg,sha256=KXabvQhqIWen_t2SvZuddFYa3S0iI3W8cAKm3s1fI8Q,1870
|
||||||
|
matplotlib/mpl-data/images/help_large.png,sha256=1IwEyWfGRgnoCWM-r9CJHEogTJVD5n1c8LXTK4AJ4RE,747
|
||||||
|
matplotlib/mpl-data/images/help_large.ppm,sha256=MiCSKp1Su88FXOi9MTtkQDA2srwbX3w5navi6cneAi4,6925
|
||||||
|
matplotlib/mpl-data/images/home.gif,sha256=NKuFM7tTtFngdfsOpJ4AxYTL8PYS5GWKAoiJjBMwLlU,666
|
||||||
|
matplotlib/mpl-data/images/home.pdf,sha256=e0e0pI-XRtPmvUCW2VTKL1DeYu1pvPmUUeRSgEbWmik,1737
|
||||||
|
matplotlib/mpl-data/images/home.png,sha256=IcFdAAUa6_A0qt8IO3I8p4rpXpQgAlJ8ndBECCh7C1w,468
|
||||||
|
matplotlib/mpl-data/images/home.svg,sha256=n_AosjJVXET3McymFuHgXbUr5vMLdXK2PDgghX8Cch4,1891
|
||||||
|
matplotlib/mpl-data/images/home_large.gif,sha256=k86PJCgED46sCFkOlUYHA0s5U7OjRsc517bpAtU2JSw,1422
|
||||||
|
matplotlib/mpl-data/images/home_large.png,sha256=uxS2O3tWOHh1iau7CaVV4ermIJaZ007ibm5Z3i8kXYg,790
|
||||||
|
matplotlib/mpl-data/images/matplotlib.pdf,sha256=BkSUf-2xoij-eXfpV2t7y1JFKG1zD1gtV6aAg3Xi_wE,22852
|
||||||
|
matplotlib/mpl-data/images/matplotlib.png,sha256=w8KLRYVa-voUZXa41hgJauQuoois23f3NFfdc72pUYY,1283
|
||||||
|
matplotlib/mpl-data/images/matplotlib.ppm,sha256=voTyfqUGvirNbkrsJGMwJT0z0g_KFWAnO8Hn8J4XXh8,1741
|
||||||
|
matplotlib/mpl-data/images/matplotlib.svg,sha256=QiTIcqlQwGaVPtHsEk-vtmJk1wxwZSvijhqBe_b9VCI,62087
|
||||||
|
matplotlib/mpl-data/images/matplotlib_large.png,sha256=ElRoue9grUqkZXJngk-nvh4GKfpvJ4gE69WryjCbX5U,3088
|
||||||
|
matplotlib/mpl-data/images/move.gif,sha256=FN52MptH4FZiwmV2rQgYCO2FvO3m5LtqYv8jk6Xbeyk,679
|
||||||
|
matplotlib/mpl-data/images/move.pdf,sha256=CXk3PGK9WL5t-5J-G2X5Tl-nb6lcErTBS5oUj2St6aU,1867
|
||||||
|
matplotlib/mpl-data/images/move.png,sha256=TmjR41IzSzxGbhiUcV64X0zx2BjrxbWH3cSKvnG2vzc,481
|
||||||
|
matplotlib/mpl-data/images/move.svg,sha256=_ZKpcwGD6DMTkZlbyj0nQbT8Ygt5vslEZ0OqXaXGd4E,2509
|
||||||
|
matplotlib/mpl-data/images/move_large.gif,sha256=RMIAr-G9OOY7vWC04oN6qv5TAHJxhQGhLsw_bNsvWbg,951
|
||||||
|
matplotlib/mpl-data/images/move_large.png,sha256=Skjz2nW_RTA5s_0g88gdq2hrVbm6DOcfYW4Fu42Fn9U,767
|
||||||
|
matplotlib/mpl-data/images/qt4_editor_options.pdf,sha256=2qu6GVyBrJvVHxychQoJUiXPYxBylbH2j90QnytXs_w,1568
|
||||||
|
matplotlib/mpl-data/images/qt4_editor_options.png,sha256=EryQjQ5hh2dwmIxtzCFiMN1U6Tnd11p1CDfgH5ZHjNM,380
|
||||||
|
matplotlib/mpl-data/images/qt4_editor_options.svg,sha256=E00YoX7u4NrxMHm_L1TM8PDJ88bX5qRdCrO-Uj59CEA,1244
|
||||||
|
matplotlib/mpl-data/images/qt4_editor_options_large.png,sha256=-Pd-9Vh5aIr3PZa8O6Ge_BLo41kiEnpmkdDj8a11JkY,619
|
||||||
|
matplotlib/mpl-data/images/subplots.gif,sha256=QfhmUdcrko08-WtrzCJUjrVFDTvUZCJEXpARNtzEwkg,691
|
||||||
|
matplotlib/mpl-data/images/subplots.pdf,sha256=Q0syPMI5EvtgM-CE-YXKOkL9eFUAZnj_X2Ihoj6R4p4,1714
|
||||||
|
matplotlib/mpl-data/images/subplots.png,sha256=MUfCItq3_yzb9yRieGOglpn0Y74h8IA7m5i70B63iRc,445
|
||||||
|
matplotlib/mpl-data/images/subplots.svg,sha256=8acBogXIr9OWGn1iD6mUkgahdFZgDybww385zLCLoIs,2130
|
||||||
|
matplotlib/mpl-data/images/subplots_large.gif,sha256=Ff3ERmtVAaGP9i1QGUNnIIKac6LGuSW2Qf4DrockZSI,1350
|
||||||
|
matplotlib/mpl-data/images/subplots_large.png,sha256=Edu9SwVMQEXJZ5ogU5cyW7VLcwXJdhdf-EtxxmxdkIs,662
|
||||||
|
matplotlib/mpl-data/images/zoom_to_rect.gif,sha256=mTX6h9fh2W9zmvUYqeibK0TZ7qIMKOB1nAXMpD_jDys,696
|
||||||
|
matplotlib/mpl-data/images/zoom_to_rect.pdf,sha256=SEvPc24gfZRpl-dHv7nx8KkxPyU66Kq4zgQTvGFm9KA,1609
|
||||||
|
matplotlib/mpl-data/images/zoom_to_rect.png,sha256=aNz3QZBrIgxu9E-fFfaQweCVNitGuDUFoC27e5NU2L4,530
|
||||||
|
matplotlib/mpl-data/images/zoom_to_rect.svg,sha256=1vRxr3cl8QTwTuRlQzD1jxu0fXZofTJ2PMgG97E7Bco,1479
|
||||||
|
matplotlib/mpl-data/images/zoom_to_rect_large.gif,sha256=nx5LUpTAH6ZynM3ZfZDS-wR87jbMUsUnyQ27NGkV0_c,1456
|
||||||
|
matplotlib/mpl-data/images/zoom_to_rect_large.png,sha256=V6pkxmm6VwFExdg_PEJWdK37HB7k3cE_corLa7RbUMk,1016
|
||||||
|
matplotlib/mpl-data/matplotlibrc,sha256=-CfOXSRZ7WaduxxmjJjX-_PLETPwuzleTpGhTLd7ylU,33061
|
||||||
|
matplotlib/mpl-data/sample_data/Minduka_Present_Blue_Pack.png,sha256=XnKGiCanpDKalQ5anvo5NZSAeDP7fyflzQAaivuc0IE,13634
|
||||||
|
matplotlib/mpl-data/sample_data/None_vs_nearest-pdf.png,sha256=5CPvcG3SDNfOXx39CMKHCNS9JKZ-fmOUwIfpppNXsQ0,106228
|
||||||
|
matplotlib/mpl-data/sample_data/README.txt,sha256=ABz19VBKfGewdY39QInG9Qccgn1MTYV3bT5Ph7TCy2Y,128
|
||||||
|
matplotlib/mpl-data/sample_data/aapl.npz,sha256=GssVYka_EccteiXbNRJJ5GsuqU7G8F597qX7srYXZsw,107503
|
||||||
|
matplotlib/mpl-data/sample_data/ada.png,sha256=X1hjJK1_1Nc8DN-EEhey3G7Sq8jBwQDKNSl4cCAE0uY,308313
|
||||||
|
matplotlib/mpl-data/sample_data/axes_grid/bivariate_normal.npy,sha256=DpWZ9udAh6ospYqneEa27D6EkRgORFwHosacZXVu98U,1880
|
||||||
|
matplotlib/mpl-data/sample_data/ct.raw.gz,sha256=LDvvgH-mycRQF2D29-w5MW94ZI0opvwKUoFI8euNpMk,256159
|
||||||
|
matplotlib/mpl-data/sample_data/data_x_x2_x3.csv,sha256=A0SU3buOUGhT-NI_6LQ6p70fFSIU3iLFdgzvzrKR6SE,132
|
||||||
|
matplotlib/mpl-data/sample_data/demodata.csv,sha256=MRybziqnyrqMCH9qG7Mr6BwcohIhftVG5dejXV2AX2M,659
|
||||||
|
matplotlib/mpl-data/sample_data/eeg.dat,sha256=KGVjFt8ABKz7p6XZirNfcxSTOpGGNuyA8JYErRKLRBc,25600
|
||||||
|
matplotlib/mpl-data/sample_data/embedding_in_wx3.xrc,sha256=cUqVw5vDHNSZoaO4J0ebZUf5SrJP36775abs7R9Bclg,2186
|
||||||
|
matplotlib/mpl-data/sample_data/goog.npz,sha256=QAkXzzDmtmT3sNqT18dFhg06qQCNqLfxYNLdEuajGLE,22845
|
||||||
|
matplotlib/mpl-data/sample_data/grace_hopper.jpg,sha256=qMptc0dlcDsJcoq0f-WfRz2Trjln_CTHwCiMPHrbcTA,61306
|
||||||
|
matplotlib/mpl-data/sample_data/grace_hopper.png,sha256=MCf0ju2kpC40srQ0xw4HEyOoKhLL4khP3jHfU9_dR7s,628280
|
||||||
|
matplotlib/mpl-data/sample_data/jacksboro_fault_dem.npz,sha256=1JP1CjPoKkQgSUxU0fyhU50Xe9wnqxkLxf5ukvYvtjc,174061
|
||||||
|
matplotlib/mpl-data/sample_data/logo2.png,sha256=ITxkJUsan2oqXgJDy6DJvwJ4aHviKeWGnxPkTjXUt7A,33541
|
||||||
|
matplotlib/mpl-data/sample_data/membrane.dat,sha256=q3lbQpIBpbtXXGNw1eFwkN_PwxdDGqk4L46IE2b0M1c,48000
|
||||||
|
matplotlib/mpl-data/sample_data/msft.csv,sha256=GArKb0O3DgKZRsKdJf6lX3rMSf-PCekIiBoLNdgF7Mk,3211
|
||||||
|
matplotlib/mpl-data/sample_data/percent_bachelors_degrees_women_usa.csv,sha256=TzoqamsV_N3d3lW7SKmj14zZVX4FOOg9jJcsC5U9pbA,5681
|
||||||
|
matplotlib/mpl-data/sample_data/s1045.ima.gz,sha256=MrQk1k9it-ccsk0p_VOTitVmTWCAVaZ6srKvQ2n4uJ4,33229
|
||||||
|
matplotlib/mpl-data/stylelib/Solarize_Light2.mplstyle,sha256=PECeO60wwJe2sSDvxapBJRuKGek0qLcoaN8qOX6tgNQ,1255
|
||||||
|
matplotlib/mpl-data/stylelib/_classic_test.mplstyle,sha256=XnegNNz-4tr8vnTgI1IakyHYPryuInJD1GidF9a8n6E,25458
|
||||||
|
matplotlib/mpl-data/stylelib/bmh.mplstyle,sha256=-KbhaI859BITHIoyUZIfpQDjfckgLAlDAS_ydKsm6mc,712
|
||||||
|
matplotlib/mpl-data/stylelib/classic.mplstyle,sha256=brJE6RZ114bTIVwl7yBm2sd6s0TyRrUq9t-qi--Ih20,25639
|
||||||
|
matplotlib/mpl-data/stylelib/dark_background.mplstyle,sha256=-EGmoFm_35Zk7oRp29UalT56HsOSuJbYMeQGdAATnz4,477
|
||||||
|
matplotlib/mpl-data/stylelib/fast.mplstyle,sha256=yTa2YEIIP9xi5V_G0p2vSlxghuhNwjRi9gPECMxyRiM,288
|
||||||
|
matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle,sha256=WNUmAFuBPcqQPVgt6AS1ldy8Be2XO01N-1YQL__Q6ZY,832
|
||||||
|
matplotlib/mpl-data/stylelib/ggplot.mplstyle,sha256=xhjLwr8hiikEXKy8APMy0Bmvtz1g0WnG84gX7e9lArs,957
|
||||||
|
matplotlib/mpl-data/stylelib/grayscale.mplstyle,sha256=KCLg-pXpns9cnKDXKN2WH6mV41OH-6cbT-5zKQotSdw,526
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-bright.mplstyle,sha256=pDqn3-NUyVLvlfkYs8n8HzNZvmslVMChkeH-HtZuJIc,144
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-colorblind.mplstyle,sha256=eCSzFj5_2vR6n5qu1rHE46wvSVGZcdVqz85ov40ZsH8,148
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-dark-palette.mplstyle,sha256=p5ABKNQHRG7bk4HXqMQrRBjDlxGAo3RCXHdQmP7g-Ng,142
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-dark.mplstyle,sha256=I4xQ75vE5_9X4k0cNDiqhhnF3OcrZ2xlPX8Ll7OCkoE,667
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-darkgrid.mplstyle,sha256=2bXOSzS5gmPzRBrRmzVWyhg_7ZaBRQ6t_-O-cRuyZoA,670
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-deep.mplstyle,sha256=44dLcXjjRgR-6yaopgGRInaVgz3jk8VJVQTbBIcxRB0,142
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-muted.mplstyle,sha256=T4o3jvqKD_ImXDkp66XFOV_xrBVFUolJU34JDFk1Xkk,143
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-notebook.mplstyle,sha256=PcvZQbYrDdducrNlavBPmQ1g2minio_9GkUUFRdgtoM,382
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-paper.mplstyle,sha256=n0mboUp2C4Usq2j6tNWcu4TZ_YT4-kKgrYO0t-rz1yw,393
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-pastel.mplstyle,sha256=8nV8qRpbUrnFZeyE6VcQ1oRuZPLil2W74M2U37DNMOE,144
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-poster.mplstyle,sha256=dUaKqTE4MRfUq2rWVXbbou7kzD7Z9PE9Ko8aXLza8JA,403
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-talk.mplstyle,sha256=7FnBaBEdWBbncTm6_ER-EQVa_bZgU7dncgez-ez8R74,403
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-ticks.mplstyle,sha256=CITZmZFUFp40MK2Oz8tI8a7WRoCizQU9Z4J172YWfWw,665
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-white.mplstyle,sha256=WjJ6LEU6rlCwUugToawciAbKP9oERFHr9rfFlUrdTx0,665
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn-whitegrid.mplstyle,sha256=ec4BjsNzmOvHptcJ3mdPxULF3S1_U1EUocuqfIpw-Nk,664
|
||||||
|
matplotlib/mpl-data/stylelib/seaborn.mplstyle,sha256=_Xu6qXKzi4b3GymCOB1b1-ykKTQ8xhDliZ8ezHGTiAs,1130
|
||||||
|
matplotlib/mpl-data/stylelib/tableau-colorblind10.mplstyle,sha256=BsirZVd1LmPWT4tBIz6loZPjZcInoQrIGfC7rvzqmJw,190
|
||||||
|
matplotlib/offsetbox.py,sha256=KZdfpLd6rYRjmRAboaRBuKag8lDUCzMEeykgmp4irx8,55449
|
||||||
|
matplotlib/patches.py,sha256=wUAZISlGrUWm_bxDh88ryGoWkOXrJZttyanOnZWdDQ4,151034
|
||||||
|
matplotlib/path.py,sha256=Ys7EYJOyCTAQm0fROCvHdgg2Kxp5AZwkJhG42hw3QZc,37139
|
||||||
|
matplotlib/patheffects.py,sha256=l7huZDCPG8RS3LLq67EztQg7s64zngoUN1R60zxpp5k,14139
|
||||||
|
matplotlib/projections/__init__.py,sha256=-_LPKYaHOdfgxYdCtPs-nDJvjQ8ZTXIuAcC0v_1_zFY,2874
|
||||||
|
matplotlib/projections/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/projections/__pycache__/geo.cpython-36.pyc,,
|
||||||
|
matplotlib/projections/__pycache__/polar.cpython-36.pyc,,
|
||||||
|
matplotlib/projections/geo.py,sha256=EI7YSw7UbZTsb31dCTAH0aJRXQct-cOnm45alvAj3Cc,18909
|
||||||
|
matplotlib/projections/polar.py,sha256=6X8t4p4LAx8h7O7WJ-T5SDhbKaNsA8X6dcXIl4z4wgM,55006
|
||||||
|
matplotlib/pylab.py,sha256=lNqCpuoTGfjprzltmvpQ3DYthdj_FJf6PRFfeEv3HhY,10331
|
||||||
|
matplotlib/pyplot.py,sha256=5AwtjpRSBFNyRyKTVrWIWSl_EPVPyPQ6RpZdOidI1nI,110436
|
||||||
|
matplotlib/quiver.py,sha256=9eP6xfF6pOOQpQ3zxbvedZewUYE236OS9gwRNZhPvws,45910
|
||||||
|
matplotlib/rcsetup.py,sha256=aFy_3zYioc0oCEmRdvqIGlCRbSA6hj7BKKTO-k2__Gw,58203
|
||||||
|
matplotlib/sankey.py,sha256=tMlZvy0CmG55hpDVY6Dqk461x6vvSxvhDmkIXJG_qDo,38734
|
||||||
|
matplotlib/scale.py,sha256=l9r0dmYGeMCLkqcrcJRIBFj6tJq6Znvc9ZoRTynHLoI,19164
|
||||||
|
matplotlib/sphinxext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||||
|
matplotlib/sphinxext/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/__pycache__/mathmpl.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/__pycache__/plot_directive.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/mathmpl.py,sha256=sdWYQ5aBB9hGiEY5E3j9By-DaFENDNHtTHxLMRQXrWc,3919
|
||||||
|
matplotlib/sphinxext/plot_directive.py,sha256=uMBuVvlmjuLUr6r1iDm42L1yN7A1r_knUXrcfxPtTBU,27082
|
||||||
|
matplotlib/sphinxext/tests/__init__.py,sha256=gTnBimTaMh3t3WC49SvyeSH0rsbWxQgjAsr5H8HRDig,23
|
||||||
|
matplotlib/sphinxext/tests/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/tests/__pycache__/conftest.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/tests/__pycache__/test_tinypages.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/tests/conftest.py,sha256=Ph6QZKdfAnkPwU52StddC-uwtCHfANKX1dDXgtX122g,213
|
||||||
|
matplotlib/sphinxext/tests/test_tinypages.py,sha256=uU72lyon36bSL2xF_90zfkblEolK_Cj5VGrfEpv0_wU,2058
|
||||||
|
matplotlib/sphinxext/tests/tinypages/__pycache__/conf.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/tests/tinypages/__pycache__/range4.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/tests/tinypages/__pycache__/range6.cpython-36.pyc,,
|
||||||
|
matplotlib/sphinxext/tests/tinypages/_static/README.txt,sha256=1nnoizmUuHn5GKx8RL6MwJPlkyGmu_KHhYIMTDSWUNM,303
|
||||||
|
matplotlib/sphinxext/tests/tinypages/conf.py,sha256=0_a4wyqPA9oaOFpLLpSEzkZI-hwtyRbqLWBx9nf0sLA,8432
|
||||||
|
matplotlib/sphinxext/tests/tinypages/index.rst,sha256=kLSy7c3SoIAVsKOFkbzB4zFVzk3HW3d_rJHlHcNGBAg,446
|
||||||
|
matplotlib/sphinxext/tests/tinypages/range4.py,sha256=fs2krzi9sY9ysmJRQCdGs_Jh1L9vDXDrNse7c0aX_Rw,81
|
||||||
|
matplotlib/sphinxext/tests/tinypages/range6.py,sha256=a2EaHrNwXz4GJqhRuc7luqRpt7sqLKhTKeid9Drt2QQ,281
|
||||||
|
matplotlib/sphinxext/tests/tinypages/some_plots.rst,sha256=C9rwV9UVlhFvxm8VqV6PoAP1AQ8Kk0LGZI9va4joif0,2156
|
||||||
|
matplotlib/spines.py,sha256=dctV_-aWkJuc52EgZof2aPjcelWVKOXttz66sZyrPkk,21217
|
||||||
|
matplotlib/stackplot.py,sha256=4YbKAU349muVPKAkNSdt9r0tG_hcHj5iCy1Dp9otWGA,3977
|
||||||
|
matplotlib/streamplot.py,sha256=_ARll6zMPC0-eLPBW-svHOs4wcG8W2tTABIcFWVwN3I,21933
|
||||||
|
matplotlib/style/__init__.py,sha256=EExOAUAq3u_rscUwkfKtZoEgLA5npmltCrYZOP9ftjw,67
|
||||||
|
matplotlib/style/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/style/__pycache__/core.cpython-36.pyc,,
|
||||||
|
matplotlib/style/core.py,sha256=8LwOz9KtNuYLOzYiUyeUJNN_w-y_LsAGRhTN_SvCftA,7901
|
||||||
|
matplotlib/table.py,sha256=fp3kcp88hOthPbc02LwZbnvbQ3__vne_LpOk2Bw_e2o,21917
|
||||||
|
matplotlib/testing/__init__.py,sha256=z-NqrY_YBuiQGl4QVqRYAGOZcyGnSIcI16XjAbmIsjM,1498
|
||||||
|
matplotlib/testing/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/__pycache__/compare.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/__pycache__/conftest.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/__pycache__/decorators.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/__pycache__/determinism.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/__pycache__/disable_internet.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/__pycache__/exceptions.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/compare.py,sha256=vklvca9q772lqZ7wsaHxI2n3MY65MCHnKTjFKV48R6s,17552
|
||||||
|
matplotlib/testing/conftest.py,sha256=NYJUijf2Rm25TwLysGfCNFKPEtD_zK9sWG5lcfUZS58,3253
|
||||||
|
matplotlib/testing/decorators.py,sha256=zOMJfeD75zD8OA5MrxsRCTOLiDGlZaMERNGw5LyBrN8,17704
|
||||||
|
matplotlib/testing/determinism.py,sha256=4GADMpjbO3127YtWNK2bNB2HTUWUkyEEQB397LGCpB0,4389
|
||||||
|
matplotlib/testing/disable_internet.py,sha256=YE5szJX8tNHyK8j90uPUOJO76MmUHKrXFqRnj_xGyu0,4747
|
||||||
|
matplotlib/testing/exceptions.py,sha256=72QmjiHG7DwxSvlJf8mei-hRit5AH3NKh0-osBo4YbY,138
|
||||||
|
matplotlib/testing/jpl_units/Duration.py,sha256=FDR6rn_Yvc3XlfLuJe4KI6ALMSJCytZGZX5xIQz-yuc,6875
|
||||||
|
matplotlib/testing/jpl_units/Epoch.py,sha256=1Htkm5XtuToVC5otGBWgsOlhPVH4FcV8EA9CkNHNwI8,7859
|
||||||
|
matplotlib/testing/jpl_units/EpochConverter.py,sha256=NbxO32pLLg7hMuiiB2QiAuiipGAfT6q2tSSf9Xys26o,5424
|
||||||
|
matplotlib/testing/jpl_units/StrConverter.py,sha256=g2EEkhg4ZdT8PSB-4MjPDNRbilRp7Wi72mTnsv7Ty7g,5295
|
||||||
|
matplotlib/testing/jpl_units/UnitDbl.py,sha256=b_4G6NaHJUl4Xd-NE1BZzombIHyOGO0i6Vb0MQGBeuw,9725
|
||||||
|
matplotlib/testing/jpl_units/UnitDblConverter.py,sha256=9AIYgnnR78G0E0D3rI20IgTawiCYC2bA_izCmy5csNo,5419
|
||||||
|
matplotlib/testing/jpl_units/UnitDblFormatter.py,sha256=bE8adtYRXG5gzQObrzR-ROLwkFkpru7GDjjUIlRh7Ss,1416
|
||||||
|
matplotlib/testing/jpl_units/__init__.py,sha256=fWVROJbodccLPtdnFzhV8ItE1Dl1uinQc9HcYz4hmpE,3062
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/Duration.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/Epoch.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/EpochConverter.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/StrConverter.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/UnitDbl.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/UnitDblConverter.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/UnitDblFormatter.cpython-36.pyc,,
|
||||||
|
matplotlib/testing/jpl_units/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__init__.py,sha256=g3AQvrbsKOFFnkrCoQzlqr1cXCDV0LvGsPnvPhNOrYs,463
|
||||||
|
matplotlib/tests/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/conftest.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_afm.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_agg.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_animation.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_arrow_patches.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_artist.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_axes.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_bases.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_nbagg.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_pdf.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_pgf.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_ps.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_qt4.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_qt5.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_svg.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backend_tools.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_backends_interactive.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_basic.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_bbox_tight.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_category.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_cbook.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_collections.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_colorbar.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_colors.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_compare_images.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_constrainedlayout.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_container.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_contour.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_cycles.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_dates.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_dviread.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_figure.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_font_manager.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_gridspec.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_image.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_legend.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_lines.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_marker.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_mathtext.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_matplotlib.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_mlab.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_offsetbox.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_patches.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_path.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_patheffects.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_pickle.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_png.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_preprocess_data.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_pyplot.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_quiver.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_rcparams.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_sankey.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_scale.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_simplification.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_skew.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_spines.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_streamplot.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_style.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_subplots.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_table.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_texmanager.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_text.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_ticker.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_tightlayout.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_transforms.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_triangulation.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_ttconv.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_type1font.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_units.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_usetex.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/__pycache__/test_widgets.cpython-36.pyc,,
|
||||||
|
matplotlib/tests/cmr10.pfb,sha256=_c7eh5QBjfXytY8JBfsgorQY7Y9ntz7hJEWFXfvlsb4,35752
|
||||||
|
matplotlib/tests/conftest.py,sha256=QtpdWPUoXL_9F8WIytDc3--h0nPjbo8PToig7svIT1Y,258
|
||||||
|
matplotlib/tests/mpltest.ttf,sha256=Jwb2O5KRVk_2CMqnhL0igeI3iGQCY3eChyS16N589zE,2264
|
||||||
|
matplotlib/tests/test_afm.py,sha256=64Qvm_dkFOh88o8oBouswePl1kgSUE_37jrRN0__jng,2218
|
||||||
|
matplotlib/tests/test_agg.py,sha256=aorEpO-NshLjutQQV-yF1ZikLIqss9fF7ouXsjG7o3s,7299
|
||||||
|
matplotlib/tests/test_animation.py,sha256=rMZwOf3WEVT7xGYDYc2xkx6XjW8_mQ0kFL3wouIaZ0g,8267
|
||||||
|
matplotlib/tests/test_arrow_patches.py,sha256=dfRDXMnKZt0FBY5JAtjKx8qbCnHCzAQoFhuEy3EzhPg,5950
|
||||||
|
matplotlib/tests/test_artist.py,sha256=PjaIk-F4jN0QEhPm3ZinWIMOB04LesHnMe0xmLmlm4g,9203
|
||||||
|
matplotlib/tests/test_axes.py,sha256=QQbwWolkRT1jytrYIp1tzsJN1tbpqF7tL0M4mIAa148,189994
|
||||||
|
matplotlib/tests/test_backend_bases.py,sha256=_jib8QrQa98qAarlE01aIk7Hkfs1gQbcVDIMGzsVr2U,3585
|
||||||
|
matplotlib/tests/test_backend_nbagg.py,sha256=TvfgH4gOzFxGRvNohjn1c9vBqEKL4OI6IWwBnZdBWgg,1066
|
||||||
|
matplotlib/tests/test_backend_pdf.py,sha256=6D7Yah7exx8xFwGsfm5ZcjYcxnRtZs0D8l3z1UZ7qPI,7925
|
||||||
|
matplotlib/tests/test_backend_pgf.py,sha256=GEYeAku09gn3erePaZMHRWVUBAFwuzdAXmrnFl5a4mc,8157
|
||||||
|
matplotlib/tests/test_backend_ps.py,sha256=KX7Xak4Yb2UMD6KEiM1-UJTtA5MwbSYhRg3l5gyofWI,4505
|
||||||
|
matplotlib/tests/test_backend_qt4.py,sha256=gNbJ4BFnwhZ-LGZ9HlXMYtQPsgPAHoYSCs8TDCFmPUA,4199
|
||||||
|
matplotlib/tests/test_backend_qt5.py,sha256=Pf4B8pNrh8qeh-vKeVeip6_fr2UhCy1fH6HDxLC0Edg,6574
|
||||||
|
matplotlib/tests/test_backend_svg.py,sha256=fbp1hAcPoxetMsfRdZ50ZttMX-vY1eqSiBfFtCXKB80,5439
|
||||||
|
matplotlib/tests/test_backend_tools.py,sha256=C-B7NCkyWsQ5KzQEnI5Be16DsAHHZJU9P5v9--wsF-o,501
|
||||||
|
matplotlib/tests/test_backends_interactive.py,sha256=AuioLNYNgZZ4Q5HJWh58CLsX5CRW0ZJBQrPYZn6DE68,5012
|
||||||
|
matplotlib/tests/test_basic.py,sha256=jexF5eJw4gwb0hYMx6_u8wJa3hgtwKclag1Lhws_f7U,730
|
||||||
|
matplotlib/tests/test_bbox_tight.py,sha256=ZcHz4qXTRhiVOiGQgSaqQgA7IjsRh84qDYOoVe82IH4,3280
|
||||||
|
matplotlib/tests/test_category.py,sha256=HJ1oBEZD0A6wHUnrz49kymv93UX0fkkdFCclIBrI09w,10303
|
||||||
|
matplotlib/tests/test_cbook.py,sha256=Tm8U0CgUpWthx7xQ_DicpslnOHlXnG9Wr0bO9E-YJAM,15647
|
||||||
|
matplotlib/tests/test_collections.py,sha256=no4WHkfoo6s6Mlc0hrrd_ssl9bpapGGTMJ7WsvCh8Hg,22100
|
||||||
|
matplotlib/tests/test_colorbar.py,sha256=YbP1IZp9HftMxpUa_yvVKFJexKCGabXQkZSaPam8qvM,17071
|
||||||
|
matplotlib/tests/test_colors.py,sha256=JrmZ9kvwN21bUwwa2YMAhrlURLRYDGDAA0yMlOHN5nc,25629
|
||||||
|
matplotlib/tests/test_compare_images.py,sha256=n3Uoukid0GcjyQpd6ZrqIY9u3RLNE2XAPWwtcHIsqto,3155
|
||||||
|
matplotlib/tests/test_constrainedlayout.py,sha256=easxoL_jBZe5ChE7eQcjz6aqx4LPxQde9FkRsWoOg5I,13261
|
||||||
|
matplotlib/tests/test_container.py,sha256=ijZ2QDT_GLdMwKW393K4WU6R7A5jJNaH1uhLRDwoXXI,550
|
||||||
|
matplotlib/tests/test_contour.py,sha256=2Yl9crBjJegsAEco-mo6oa31TfoJkupLbqlwl9IFkWU,12909
|
||||||
|
matplotlib/tests/test_cycles.py,sha256=75QI7uIkh4b5ckGd0B7irJuqhIivYx8CmxRAciWRj1g,7490
|
||||||
|
matplotlib/tests/test_dates.py,sha256=SDos5lnmY_dNNIT-hjVZ3e5OpmSePAOIWFJoA_9tDdM,25085
|
||||||
|
matplotlib/tests/test_dviread.py,sha256=kTk9Qv6q9Kk3fJcDAEWm35HF-sKsP6Ybec6N8jEHasE,2342
|
||||||
|
matplotlib/tests/test_figure.py,sha256=LVk5dyf6H8V8Ot2bIQXAnscaqLegeDfud1wtLXusJXo,14204
|
||||||
|
matplotlib/tests/test_font_manager.py,sha256=8dVBlum-bY9gmy_YVXWERbWtbGNQllwzCyCNxffGCFI,3271
|
||||||
|
matplotlib/tests/test_gridspec.py,sha256=zahj5Rd4pB0xtAc_3KX7fQWyBys0P-IQk-Cq0cs8VgY,626
|
||||||
|
matplotlib/tests/test_image.py,sha256=uAMkqy06wYZeJASFKIt-hdWCG310dL-AFp6cnLdnjlM,28263
|
||||||
|
matplotlib/tests/test_legend.py,sha256=2Q6Qn2C72wRqZvY6Mt9v_udD0bVARDTbWcqBZD1-oCk,19782
|
||||||
|
matplotlib/tests/test_lines.py,sha256=4n7pdODOXqLvX1zing9SuXXxChPDe7nEr42gaiIG8d8,6221
|
||||||
|
matplotlib/tests/test_marker.py,sha256=fhHHW93wCl5KbrZRL2iEVziv2BLBZU6zSt66hgsI5jY,739
|
||||||
|
matplotlib/tests/test_mathtext.py,sha256=3G0W1S2J5QOHs0EfQiAdP5M114sh81sJtDxITrF1gzs,12696
|
||||||
|
matplotlib/tests/test_matplotlib.py,sha256=DIBqISzUIYanSxNWJL9n2oob1dRLOOAr6TIz2BTWK1I,706
|
||||||
|
matplotlib/tests/test_mlab.py,sha256=gpd4pJ8fIQNCXdwJV1C1__EKBMdnkvQLVgyFdgNgrZc,96981
|
||||||
|
matplotlib/tests/test_offsetbox.py,sha256=u_VCL-lWvQ66IGTzuotzRxq23ySzcB86Gjvmeib-S3w,4198
|
||||||
|
matplotlib/tests/test_patches.py,sha256=IbeCN3mVunvkukneSZKjVbZKGJg0h2oqmOzrI400HQ0,16218
|
||||||
|
matplotlib/tests/test_path.py,sha256=EwvnGhKmdMFl5w4PC2KWwKVm2qKLuRksM70kb-MUVTU,7164
|
||||||
|
matplotlib/tests/test_patheffects.py,sha256=Sl90AY4wri37bEvRuS3QEthE_GV5i5drh0Yk2oGJjc0,5372
|
||||||
|
matplotlib/tests/test_pickle.py,sha256=38XMrLSsH7RgNVesFLB68UAD6HZS7Eylbs3DktWmkBw,5533
|
||||||
|
matplotlib/tests/test_png.py,sha256=3TieTdSRvSL9mS7bhxpozw1CClQxKYunK83TLPU4bnA,1786
|
||||||
|
matplotlib/tests/test_preprocess_data.py,sha256=oJuYg8kpqgLrA1wYEUzMLULRoKM3IRf9_ejRzPzM0AU,15990
|
||||||
|
matplotlib/tests/test_pyplot.py,sha256=wOld704X_pAZVNvnL3WD2KUHliRPtAoqLBh0-OrdeGg,974
|
||||||
|
matplotlib/tests/test_quiver.py,sha256=hAKbP4S6npdpIfwjw6efhMr6En2A-0fJAjLOiMofxWk,5789
|
||||||
|
matplotlib/tests/test_rcparams.py,sha256=bmDAFAz3g0ZLHLVqVU9F4POt-X5HbAqpLNfBp4m81N8,20498
|
||||||
|
matplotlib/tests/test_rcparams.rc,sha256=zwPbYzajd7FTIYURvpwTBAn8i060Do3OVDGZ2xHZeLw,74
|
||||||
|
matplotlib/tests/test_sankey.py,sha256=ZZBtNqIsFcJLoZTCKSM2xaUfrtMjoSica3Mi-FtMysw,162
|
||||||
|
matplotlib/tests/test_scale.py,sha256=8V-tt5E79R-P_Zz6e7iH_7KCHU6e1Ql2SLnQdpt7BmQ,4100
|
||||||
|
matplotlib/tests/test_simplification.py,sha256=c1GSkJwiGxksnUj5WgYbvsr3yDcmbEKHmB_12W_jp7s,10895
|
||||||
|
matplotlib/tests/test_skew.py,sha256=zOhGb5V-9A531ZpmHlqFsdTL9xfdj2-RL3N7OQJbV20,7058
|
||||||
|
matplotlib/tests/test_spines.py,sha256=1cN5KequShVG83DggeUxt5QWE9uIkfmyONaoVNJOojw,2326
|
||||||
|
matplotlib/tests/test_streamplot.py,sha256=F-ilyMfXm47d-XCz6L06GE8KQ-bEHbhKLqIS0EVKv0I,3497
|
||||||
|
matplotlib/tests/test_style.py,sha256=JAcu_9wDS_Gvtisce9Z6rHrO-SHZVjyPeBDYfKgDZ1c,5333
|
||||||
|
matplotlib/tests/test_subplots.py,sha256=yMzfFiUpZBcBt2FpLFySHzxmoj2B4jevjLviFWkPouo,5551
|
||||||
|
matplotlib/tests/test_table.py,sha256=Qoe4Sm-yyog-NOHASdBOjjGQrFQOo2amm01KAgP5MJo,5906
|
||||||
|
matplotlib/tests/test_texmanager.py,sha256=zCtJ3JnZNfP2AQNy7q2LQAgaflSe7S5htJkJNylQSGE,459
|
||||||
|
matplotlib/tests/test_text.py,sha256=Vg5HlZU9ob584X5AZAxQuIm6PFj_GO9xnxIBKZU7O2c,15435
|
||||||
|
matplotlib/tests/test_ticker.py,sha256=OPuJl75xgr_AZ8IDUuyi90PgAXBVT91EnORB2h31mUQ,32802
|
||||||
|
matplotlib/tests/test_tightlayout.py,sha256=7w_LPOY1EM0Z6i7GPt1--w4z0wmf-5M6rvpd0wauqKg,10016
|
||||||
|
matplotlib/tests/test_transforms.py,sha256=Y3FWBzEiBBydD5X4id-bgpzRSFCBxyZFtPTbto2KUv0,24713
|
||||||
|
matplotlib/tests/test_triangulation.py,sha256=XXCtxqhP6jHFe2mbIbB0-KNsdkzxhUDI1V9bDz1A_Eg,44838
|
||||||
|
matplotlib/tests/test_ttconv.py,sha256=xilgvzZpTpHSnumaUlHvQ_zdIQ7U7xBD1Aflx34I-xU,641
|
||||||
|
matplotlib/tests/test_type1font.py,sha256=C0pCPBGOv49SR2xxDOq6LSXAEH_ZNvIWvr_jG-23Gmc,2097
|
||||||
|
matplotlib/tests/test_units.py,sha256=yUnl2ds8QGdJdZWME2YHysQbSr9DG4aEkgIIstMZyMc,5196
|
||||||
|
matplotlib/tests/test_usetex.py,sha256=9ANPkY6aKfyY0_DopPLivplxlT__v5y5wHX2_8-Z4Z8,918
|
||||||
|
matplotlib/tests/test_utf32_be_rcparams.rc,sha256=K66jcKehDKcG1yTXJCOSsmp1iteU9Kvsd_eobV5wNW4,56
|
||||||
|
matplotlib/tests/test_widgets.py,sha256=7sBak0W8XT-NhZ9zNEZC8i_PA6LJIkBgiE_gB42h2cE,16787
|
||||||
|
matplotlib/texmanager.py,sha256=KBU8HQhgj0DNOTwpCZhH_pJ1IqyfEnF1IgUOBDUpRqQ,17284
|
||||||
|
matplotlib/text.py,sha256=-q9B-fS_GmCoAEHhEDbY7m9fidyFLjSn3600KAcROkE,81684
|
||||||
|
matplotlib/textpath.py,sha256=FLg0qVzTVf9_CFbE3O39Pw7-4hiAYiWoqz_dwKMmHYY,17917
|
||||||
|
matplotlib/ticker.py,sha256=Pb6Eyaiz6ncDoDjE7Vew8h63i1s-x9hNR-aRHP9-fhs,88149
|
||||||
|
matplotlib/tight_bbox.py,sha256=Yf5X-HVlMe3AqwR5tUJQBH0LLZqOGoh2uP0oJnU6ErA,2463
|
||||||
|
matplotlib/tight_layout.py,sha256=iQt1J_nr40VPf3GCgqed_r_umsYE5lnvjWxLdh3-uPs,14562
|
||||||
|
matplotlib/transforms.py,sha256=2pDKJqB7Fnx2WLvWUKY8wG7ry9xk0xtVq9dDu32HvLc,99669
|
||||||
|
matplotlib/tri/__init__.py,sha256=XMaejh88uov7Neu7MuYMyaNQqaxg49nXaiJfvjifrRM,256
|
||||||
|
matplotlib/tri/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/triangulation.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/tricontour.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/trifinder.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/triinterpolate.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/tripcolor.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/triplot.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/trirefine.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/__pycache__/tritools.cpython-36.pyc,,
|
||||||
|
matplotlib/tri/triangulation.py,sha256=YM3AIH44SK4LOJiLPB4Wo-DQ1d2q75SdUQ_W2kaxrO4,8427
|
||||||
|
matplotlib/tri/tricontour.py,sha256=Uz3bHK7xw3fHnjFstOlurnz9-btm6dfa0lKE7Vt_P0k,9375
|
||||||
|
matplotlib/tri/trifinder.py,sha256=_S-whwBCe5m9byOzcdAXFJXs0gAIXqy9rVGkXKiM14U,3505
|
||||||
|
matplotlib/tri/triinterpolate.py,sha256=uWh1PPiaN0nMM30txiEVoAxtFZXvX0hxvPnWzKP3xoc,64969
|
||||||
|
matplotlib/tri/tripcolor.py,sha256=DwBFSsZ_jBrFKIPrYetMXNqy_i9GS9-BQUDjPig2WOw,5326
|
||||||
|
matplotlib/tri/triplot.py,sha256=aZ9O_VVLH0AOne31u11ltLlyVyhqKtyzec7WH3b3pkk,2857
|
||||||
|
matplotlib/tri/trirefine.py,sha256=DZS_gihMxkUMzuxAijKnEDo4Po_ahIHY7-uGnjUY1Eg,14142
|
||||||
|
matplotlib/tri/tritools.py,sha256=dC_OcwrFN3gunCe3SgHjQTH_dHBCncM1Fex0bQ_b1Jg,12498
|
||||||
|
matplotlib/ttconv.cpython-36m-x86_64-linux-gnu.so,sha256=tnAP4SNjDwMwdCZFJjzoDYLYa0wf3LAMZmKd9M_QmS4,83888
|
||||||
|
matplotlib/type1font.py,sha256=aBak-e5VKpZpH_LyYqNUyAX4vgRisZq4sfeEcV45-j4,12173
|
||||||
|
matplotlib/units.py,sha256=URBYLJTwt1n-w7A0LcznVlMCMMnLaJFEVrhEHTt0Yv8,6665
|
||||||
|
matplotlib/widgets.py,sha256=vAYlyIJUze6G7CW9CUmY1LePR5L5XFJrPqvVIYycWzY,94072
|
||||||
|
mpl_toolkits/axes_grid/__init__.py,sha256=d0j8ET68OmR22G59uzWO4BQ3Jv2kCmJC15nZRAf227M,559
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/anchored_artists.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/angle_helper.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axes_divider.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axes_grid.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axes_rgb.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axes_size.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axis_artist.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axisline_style.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/axislines.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/clip_path.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/colorbar.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/floating_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/grid_finder.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/grid_helper_curvelinear.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/inset_locator.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/__pycache__/parasite_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid/anchored_artists.py,sha256=_F6-9iacZidb5JpJ8jCOZ9PdiZaR5qpfBjf-3VjTzNc,291
|
||||||
|
mpl_toolkits/axes_grid/angle_helper.py,sha256=Tb4Mb_NGkUdkisebe2dqfBdFmUZiSmGyUnftiSeSIls,51
|
||||||
|
mpl_toolkits/axes_grid/axes_divider.py,sha256=Q1NvDXXtKVuX7iUoKFFRw11Wg1eHEGt3-qDWG7DOVxg,269
|
||||||
|
mpl_toolkits/axes_grid/axes_grid.py,sha256=t2Fc8fM-_qINumuDxctOEYhMI3M1ZfqEVc3th-cnz5g,152
|
||||||
|
mpl_toolkits/axes_grid/axes_rgb.py,sha256=nKv0IWpKHN2NW5eZqx_rbaZqqMWvYw9GK94gIAVEmE0,301
|
||||||
|
mpl_toolkits/axes_grid/axes_size.py,sha256=v4Nhxe7DVp1FkKX03DqJJ1aevDanDvgKT9r0ouDzTxw,48
|
||||||
|
mpl_toolkits/axes_grid/axis_artist.py,sha256=zUlJFUHueDsMtzLi_mK2_Wf-nSBQgiTsMOFpo_SngZ0,50
|
||||||
|
mpl_toolkits/axes_grid/axisline_style.py,sha256=lNVHXkFWhSWPXOOfF-wlVkDPzmzuStJyJzF-NS5Wf_g,53
|
||||||
|
mpl_toolkits/axes_grid/axislines.py,sha256=kVyhb6laiImmuNE53QTQh3kgxz0sO1mcSMpnqIdjylA,48
|
||||||
|
mpl_toolkits/axes_grid/clip_path.py,sha256=s-d36hUiy9I9BSr9wpxjgoAACCQrczHjw072JvArNvE,48
|
||||||
|
mpl_toolkits/axes_grid/colorbar.py,sha256=DckRf6tadLeTNjx-Zk1u3agnSGZgizDjd0Dxw1-GRdw,171
|
||||||
|
mpl_toolkits/axes_grid/floating_axes.py,sha256=i35OfV1ZMF-DkLo4bKmzFZP6LgCwXfdDKxYlGqjyKOM,52
|
||||||
|
mpl_toolkits/axes_grid/grid_finder.py,sha256=Y221c-Jh_AFd3Oolzvr0B1Zrz9MoXPatUABQdLsFdpw,50
|
||||||
|
mpl_toolkits/axes_grid/grid_helper_curvelinear.py,sha256=nRl_B-755X7UpVqqdwkqc_IwiTmM48z3eOMHuvJT5HI,62
|
||||||
|
mpl_toolkits/axes_grid/inset_locator.py,sha256=qqXlT8JWokP0kV-8NHknZDINtK-jbXfkutH_1tcRe_o,216
|
||||||
|
mpl_toolkits/axes_grid/parasite_axes.py,sha256=kCFtaRTd0O8ePL78GOYvhEKqn8rE9bk61v0kVgMb6UE,469
|
||||||
|
mpl_toolkits/axes_grid1/__init__.py,sha256=SEWPa2ggZnKkFVX4yaJOPN7KgyV_T-cyjr8UjIjjhPs,272
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/anchored_artists.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/axes_divider.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/axes_grid.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/axes_rgb.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/axes_size.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/colorbar.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/inset_locator.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/mpl_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/__pycache__/parasite_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axes_grid1/anchored_artists.py,sha256=SPXVgw8CLMGTyPScN3Q2WHeWJbhtQo52Fo3DaLJ8yrY,21162
|
||||||
|
mpl_toolkits/axes_grid1/axes_divider.py,sha256=RNaghesva1N6F4h6J-5Amy15LVqzQyW-FelkfQ_m9n8,29873
|
||||||
|
mpl_toolkits/axes_grid1/axes_grid.py,sha256=BOh_MSGrTK9_-rAFuY5UST84PRDLObCj2tOg71wYjXI,27243
|
||||||
|
mpl_toolkits/axes_grid1/axes_rgb.py,sha256=UlErJuhcqtN0skRLckf_v-GvUtAWDTVW401wUzwXOxI,6603
|
||||||
|
mpl_toolkits/axes_grid1/axes_size.py,sha256=m4LSknVO9c6vcpT1bEZBKYUGkoJ7BOrPnLRTcLmnmFQ,8933
|
||||||
|
mpl_toolkits/axes_grid1/colorbar.py,sha256=BLNBORudFV18ShwQiiVdceUOTJESQZ4yefVu23yVex0,27428
|
||||||
|
mpl_toolkits/axes_grid1/inset_locator.py,sha256=EeLwbA6sUhCBFzPVA1MwCTGE8N1APdKZXq4xSiCXG1Y,23722
|
||||||
|
mpl_toolkits/axes_grid1/mpl_axes.py,sha256=nUXeFjye-NBR1SCXYx1qirV6QIYSB2e01AEeoWlzm7w,4680
|
||||||
|
mpl_toolkits/axes_grid1/parasite_axes.py,sha256=TjNMInmARhT4tZn2PBafcecuyammO3KBuCnUUYyhbdc,12908
|
||||||
|
mpl_toolkits/axisartist/__init__.py,sha256=2zsgjqTtP_NXv78MEaKabmfmkjA7yhy77pIcaR57YWs,748
|
||||||
|
mpl_toolkits/axisartist/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/angle_helper.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/axes_divider.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/axes_grid.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/axes_rgb.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/axis_artist.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/axisline_style.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/axislines.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/clip_path.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/floating_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/grid_finder.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/grid_helper_curvelinear.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/__pycache__/parasite_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/axisartist/angle_helper.py,sha256=csNOzIHc3s1fvJm7XpuZy74kQY2oeC5przHcVSSFVLc,12932
|
||||||
|
mpl_toolkits/axisartist/axes_divider.py,sha256=FqYC72nAYkmU9oaawDb7TjMxb1NSjhbYocD1vxwCrvM,509
|
||||||
|
mpl_toolkits/axisartist/axes_grid.py,sha256=vfd_EXHuYQ7iIVK2FOm6inLhb7huZxtOSvFyOVW2GmU,610
|
||||||
|
mpl_toolkits/axisartist/axes_rgb.py,sha256=TpJCB8eA0wHZVXOxxfFoy1Tk_KFj68sZvo74doDeHYE,179
|
||||||
|
mpl_toolkits/axisartist/axis_artist.py,sha256=4IdmKz2zRHnRCerIO-lYnf7LiUphSE7fe_Hq5VapDms,44753
|
||||||
|
mpl_toolkits/axisartist/axisline_style.py,sha256=It8dzmdESmoAmwwEjOs-YjtBydKlNmb41vV6v8tZ1-s,5107
|
||||||
|
mpl_toolkits/axisartist/axislines.py,sha256=63q7XMODxvM3mwHCmBvtczaOgre-dGigNqavgrDZ844,22082
|
||||||
|
mpl_toolkits/axisartist/clip_path.py,sha256=_fxHB05pFazHxDmNRXN7xO5EfJaxFPHMwFfqwfAs2uA,3736
|
||||||
|
mpl_toolkits/axisartist/floating_axes.py,sha256=B3_1_qTFDSWIpfbRxMf9TXyoRzQmwMISdpqU46nC-Uc,16491
|
||||||
|
mpl_toolkits/axisartist/grid_finder.py,sha256=E_JrpMeAIUj9FZ6Q7_rd4cEYeDbb5TgjoOsJ_5YQIoc,11513
|
||||||
|
mpl_toolkits/axisartist/grid_helper_curvelinear.py,sha256=yH4--wkTm2C2FSSc_TQcvU-24wJlJLj18JphW5Kzz80,15491
|
||||||
|
mpl_toolkits/axisartist/parasite_axes.py,sha256=1sQwBEYuXHpaEeObb7cXh0I1xWroYtcvFiEmwrzqK3w,447
|
||||||
|
mpl_toolkits/mplot3d/__init__.py,sha256=V2iPIP9VyRhoJsFWnQf5AkfyI1GSSP9H6hICEe9edJo,27
|
||||||
|
mpl_toolkits/mplot3d/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/mplot3d/__pycache__/art3d.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/mplot3d/__pycache__/axes3d.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/mplot3d/__pycache__/axis3d.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/mplot3d/__pycache__/proj3d.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/mplot3d/art3d.py,sha256=Juj1shJAObe_JDhF3Fm4qtMVPmvLMUYt2lKp8dakteQ,24879
|
||||||
|
mpl_toolkits/mplot3d/axes3d.py,sha256=EDZc-nuqbI6HNt7oioFRL36EKODH8HTA68EsElSW_II,103257
|
||||||
|
mpl_toolkits/mplot3d/axis3d.py,sha256=z8Q20DsMzFvA-jYWFsbbo-fFB7Yzegdlg7Wz1Ws5VPE,18574
|
||||||
|
mpl_toolkits/mplot3d/proj3d.py,sha256=JNb8VcfoAOwRJMLAOT6pdX2PDE7fCx5L48PGdIACzvU,4612
|
||||||
|
mpl_toolkits/tests/__init__.py,sha256=iPdasxJf0vpIi11tQ98OVSQgS0UaPUyOEGGfAryAhIA,381
|
||||||
|
mpl_toolkits/tests/__pycache__/__init__.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/conftest.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axes_grid.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axes_grid1.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_angle_helper.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_axis_artist.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_axislines.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_clip_path.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_floating_axes.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_grid_finder.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_axisartist_grid_helper_curvelinear.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/__pycache__/test_mplot3d.cpython-36.pyc,,
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid/imagegrid_cbar_mode.png,sha256=yvo6erXXc3Z9aO0rrEezBooCc6KhAw7wKv4WngOQmFA,87393
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows.png,sha256=XMZGgG7_9k96bKhI2G--XBVKpct5O5psbGH2Wvj5YA0,10784
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows_many_args.png,sha256=fkPsdmhd4S1g-QxMb55M63iAgWmC2G4ytcLOT9tMAD0,11039
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/divider_append_axes.pdf,sha256=eW2CuM_T4d95dC-DU0PmmQD7gqRQIO0rcQpvp-zu1i4,25446
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/divider_append_axes.png,sha256=VfRfs6p4akgjGxxNm6Bu83Pg0v1KmU7WPu97_-kzNFc,48825
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/divider_append_axes.svg,sha256=usfsa3y-s-N2KMOzsOZHTq-PZXgAPXsSM-lkxJ3ZUi0,172812
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/fill_facecolor.png,sha256=Tkrylxebxm8SuWZjQK0qXSX8m9QsQU6kYm7L2dgt4yg,14845
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png,sha256=EStruex2GqxBIGm49SXqrn8lO4-_XhsAnvs5CmtUawc,3872
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_axes.png,sha256=RQmR39E6Vskvl7G4LInHibW9E1VK0QgCvI-hBlb-E2E,9928
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_locator.png,sha256=bQKKKUuoU_EZwZT_9FzzeVKsKwUUBOZV55g4vVUbnCU,9490
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/inverted_zoomed_axes.png,sha256=rvglsLg8Kl9jE_JukTJ5B3EHozsIYJsaYA0JIOicZL8,25997
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/twin_axes_empty_and_removed.png,sha256=0YzkFhxs4SBG_FEmnWB10bXIxl9aq7WJveQAqHm0JrQ,37701
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axes_grid1/zoomed_axes.png,sha256=mUu8zJtz8FMb7h5l4Cfp3oBi9jaNR5OoyaDgwqpAZp4,25893
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist.png,sha256=qdlk9UPScCAN9RBOhoNqLmJvmkXt8pCuwuQtrz5E8Bs,10151
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_labelbase.png,sha256=An5lovtvAiNA1NZI-E8kOj6eYTruQMqwf3J7pXwdk4A,10598
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticklabels.png,sha256=7vuAKkIqcpgJrc2AF7oslf-E_sDfSlCoymyc87u4AWs,5696
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticks.png,sha256=CkVtCWG13ViW0w2DsbzfXSvoFWHYaaqQYeEYpbKbOg8,5695
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axislines/ParasiteAxesAuxTrans_meshplot.png,sha256=FOgl-Glmzhdp6V8mz4StofTsFXGysFkEcUeaWtmJDZs,34354
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axislines/Subplot.png,sha256=tRpYCjR5zUkafA85DVmY3duTEouwCZq6jDwSF4UsBS8,26919
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_axislines/SubplotZero.png,sha256=3kCrz7HQMYrK3iDgYgf8kyigxRtIGFBbcUzJPtiXh_E,28682
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_clip_path/clip_path.png,sha256=BtMyb7ZawcgId9jl1_qW72lU_ZyxLN780uQ9bCLjbHA,25701
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear3.png,sha256=4th7Y74_9YV6X25RqJW0Op9WDzGRCcxF1kfNogkgozE,52835
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear4.png,sha256=cYjrSiH6Mvor-VhmwNUgX7Le3_k1rurpd8L5vhTf16s,29374
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/axis_direction.png,sha256=3fue92dg-ntYI0XX0nB31IFpgRT2V3izqjrmLvEdYN4,40536
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/custom_transform.png,sha256=4cQhIFK1z8oPUVyvkHNZ_m-GCbikmUbTvkvYVGy6U4o,15118
|
||||||
|
mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/polar_box.png,sha256=wWaPM3I7_435SkVQqIggb8BHrWBMWrsSVyMZQQJ6fE4,62526
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_cla.png,sha256=htnP1CA8dd85KqdnOsHVlsehT90MUoQD8kFTyra0AuE,51409
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_labelpad.png,sha256=zrLsk8t7s970yaY3cqj6SOMbI6UY8Loe0Zbp0WqFtwQ,66817
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.pdf,sha256=zkfSOR2bJYC_X425qnXHMmGJPSlLSpFs53YB_R760Gw,6603
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.png,sha256=SoyN30SsuvEECZyB_ReGP3ZKGZJazOp05dXa3YUn7Jc,47796
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.svg,sha256=Kb_zdIzZG6JKnztMcmOUG4esPsuteljB_A2sxrhIA3Y,18046
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.pdf,sha256=YI5gzs8NK6fWo6JB0wf8xeZ-FrHlS3P8DSCccsLU4fE,7197
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.png,sha256=Qw909B4nDmV9DlMuo1DKk7y5ndjtvni5d_DcysmG9VA,100466
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.svg,sha256=iU1Pk60SDC89km6bwz9Li9mXdNdZ_Vn15bkbAUG2iKo,30591
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_notshaded.png,sha256=p9nV0cr7ZqhmM5VRHYcByR_QWKh91b8jjt71nkrq3Bc,66289
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_shaded.png,sha256=9RXKAlcPSrQYmhvqH9JMnlLaR62satjwyUq1joXcy6g,64595
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.pdf,sha256=CtzH5MJNMY_hZGAyp9py9PLI0a8kJ-jNnpQKQYtoQEE,25170
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.png,sha256=tii1IakS8MC_Vuwd95HhcyM0iq4zGN5DxgRFfB9mKu8,83161
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.svg,sha256=3VjmHaN5hRXJ-HH9duS1M6nR8gkgBOCtb3zruWcSujU,67618
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.pdf,sha256=H7FyZjuiA12CzP8FDDnVNf42HxGfMlg_8rA57HA1sIo,52812
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.png,sha256=Jb-fhAcgogE8jn9DSsaqInUfWC7D_5Pf3QRf7XWAX2Q,42575
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.svg,sha256=cz9TicdIGJ_8UdHTKQZ76n2rAv6Rx2EELPg8AyAEgMU,173077
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.pdf,sha256=raTRNOdfYPFJvZiM7BbRbdWRPElQe0sn6jsKH63TTyA,3950
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.png,sha256=dE8eHoj43eePB44F1nLM2RLj8iqw8rCYI3D0VD3gUg0,39694
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.svg,sha256=BM-PcmZJ-8iyB4wUWQxcMuskmDXemrsX52LKPYEz850,11093
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.pdf,sha256=6pKrI8lUIPxRwi3ofm8DK8UqNeZsBYprXwX51vwFv1Y,4354
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.png,sha256=DQT-NruHCeG5LKpjG-dlLln3aCoPKhua5PQnHTafBGU,60217
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.svg,sha256=c32m_X6tFYlY5PCVYJ0cPzvZDTbgRA-GIwzDUVTNKYA,12919
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.pdf,sha256=Y-C6q8kG4QdVsBpck1Qg90W87rSnbOzrDQP1v7qFZDI,53962
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.png,sha256=rGHpqTyXqt1PvCSvSrP7Dnd0uNeeEmPPgJwADxXdfM8,39763
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.svg,sha256=TQ9KQlKC0BU4lnayB2S8ArbsfAh87qKjFEh5WKjWbvk,286241
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/plot_3d_from_2d.png,sha256=AWos5EJWMerD0tgVZyvBofz-5hnCq6fhGHKmQi-izAg,56593
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.pdf,sha256=mJYYIj01eM9ouCPpoeWu-KDrXe3_o2um4_JddGAzWGg,2680
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.png,sha256=ePzSA-iFaQbmH603vw1jhs9vyIt45xXnbpIuUF3a1l8,52065
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.svg,sha256=K-D9vp-jB8KFTilVotfIQvuhG3qTMT04XZ6LUIfpZ60,8594
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube.png,sha256=AJ0EoayvdBoywpOUWcxbMQ0oB7cTzcoWGgGyx2qgQMU,23182
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube_ortho.png,sha256=5Phz7BclSciZpg4SDu-eUQ-v_ikHbEqReQWCdeHywQk,16210
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_lines_dists.png,sha256=XCd4hX2ckc5GCxcgenkRJ8MT7pX-3iMLylD2rCjNl-4,18898
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.pdf,sha256=T4nKDMCEi2zKpoq5FOsdyMQJd0igS9yBeyUL84a0oso,18589
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.png,sha256=PBllNI1kHf1rz-oPK507OwsPNE9DPwivXAVJM9DemBI,104755
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.svg,sha256=_cwXN4aH-mQx5ADarZtXsY-vaeRFg4dPuVmP6S9NZbM,128623
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_empty.pdf,sha256=DSFSucOBU22R3zmG8eKlWWtLy5Wb7L7wqZ1B7CMybKc,7405
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_empty.png,sha256=98D3k5QIL7KugUwzqJhdLtp9dgDGgx8hGa9_u8cvX6o,37954
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_empty.svg,sha256=H_b_HtjccyLTNDjIqUek6DwQugfsyb88nVBepBzFdTo,7745
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.pdf,sha256=_VIBNh32vX_K9QrH-0o4z13FAE0JZQuOzOfkCTsSe7c,10939
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.png,sha256=67yp7-6f-vDiYTmCqMFfuIEGly5UHCCUOV84YJtLsX8,80392
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.svg,sha256=JO8n2RUeQANeCpq_PmtyjmVPqMmuqvZQg7vk_3hsv8I,68796
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_middle.png,sha256=N4o26wMzfnyxndPbZ2VnsjIAiNYrFN9Aa40ficwO9AM,104735
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_tail.png,sha256=Ff_UrWxD-VIMQLN1uXy5u_Yd5e1P427YfGM05nvU2kE,104951
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.pdf,sha256=93umAGrsz8bYekxMETAlU67eCTY3tzycxpVmm2M7eEE,5847
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.png,sha256=MDaocusHz6Itinjm2j6fnDh-rl1fqVjnqM89nP8bwZs,43155
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.svg,sha256=yc-PIx36-DdoCE1Vd8JRih45GoBR7eVtdi6wMZVZUXU,12494
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d_color.png,sha256=Y7De9BIFLp0Ova4fk9IcXloNjiwmifTrFA1IfVJA3aE,41598
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.pdf,sha256=OSvMZiGJzmVdtv_e0XHIBw4bLSY7DLtWGDAYgh6vNuo,48096
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.png,sha256=Ok0UmO2DELze2yK8mRx0CifmRAgvjyS1IvERsBRvFlU,54712
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.svg,sha256=CaqTIT-jdKSKfCqp8GC_QAc_3Dxwgr97uvDR-a4xdcw,270325
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d_shaded.png,sha256=tW1q8o6BAlqGD-ILqAXXTBxEt08fwIwEnK-L7n4fHFo,43405
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.pdf,sha256=CfWlocbvwHK48CADjhNjjXJ2eHsSvoNWWpz33RvkuQQ,13906
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.png,sha256=cuahU107LG85pT3kyHngOPV2GchTUwu1AkLYbb4UDd8,77741
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.svg,sha256=MmcVy3u0Syymf933rIsaUY8XOv6b9r6ScVXx33NFxBg,36357
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/tricontour.png,sha256=8IjYmJP6cBhnPGLz-WDyn7UUMYZ10Kz2MpjOFwDUVow,71328
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.pdf,sha256=OVbK3ausYMApR7_UJPx0pf3bt4A3CpZumhAVhzQMbs0,169155
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.png,sha256=nO0gJBIluLEX3mlxXY3C6bx-9Jf_xJyXAnTXKnqrIkQ,99103
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.svg,sha256=PFUTTF34KiJcol6J5JmRjkaillzRWAgsbfxgEhpWxmA,103333
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d_shaded.png,sha256=45y5oF9MzbR07cP-T9CNF7yLWo8qUDoG3E91kc6jueE,94492
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-alpha.png,sha256=4BUCO65oRVuxgOs3i06OFVNWi_UJLelUQzJ7WCzhdOQ,179128
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-edge-style.png,sha256=DMDM2KV84Z0TH7BlWJAuESle0NbFcCsmDF9Q0rPmP3w,65236
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-named-colors.png,sha256=ze6vMiuPmRj7FOX_1ltXMWwQNaRD5jl3drzXbdMKcFs,96319
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-rgb-data.png,sha256=6X8fQ-dYqDlRt9oSoeYbic2mQb1DDlqPfAUpMbh2O9U,136235
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-simple.png,sha256=mG67qqJF-9TKYbacsbVknSnUOj8D9m0sYmy0imfwY2M,60940
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-xyz.png,sha256=OeInzIzGNXG490czp9McRMqEMcgiz0ABkUky7QlTwfE,122177
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.pdf,sha256=3gYea_CtpNg1UmurOhyWt3vHZSkLX1-trEY779yrxbc,36169
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.png,sha256=epmsR4rWGzh7prW3RL_t8ZcUEsphM5bc0t5j__iauOY,108371
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.svg,sha256=-hjWYF-WSE3aDuCSpHYztjeV_yktEqatDK2yZyn_rhE,85892
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerocstride.png,sha256=WaO_3NcaZPFzlui5SQYJ-TbUylHkSbieneCYPffNgAA,81117
|
||||||
|
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerorstride.png,sha256=y1JvfuVOBiNhJoJ2HdOXyBYBBkQm-oaPcoekfT-cMso,84284
|
||||||
|
mpl_toolkits/tests/conftest.py,sha256=Ph6QZKdfAnkPwU52StddC-uwtCHfANKX1dDXgtX122g,213
|
||||||
|
mpl_toolkits/tests/test_axes_grid.py,sha256=UCQFk5p-9sbTMCS6RKk7BfyCiTb9yRnsarH23eUem0o,1638
|
||||||
|
mpl_toolkits/tests/test_axes_grid1.py,sha256=mHdjXAd-3X1OEW0yS9iap1i2GUuvqtH4-UnnOICT_U4,15028
|
||||||
|
mpl_toolkits/tests/test_axisartist_angle_helper.py,sha256=2jLmTrH4fw3Xty2CwaBsRJISb8qxWWn8ALRa4HL2FQA,5702
|
||||||
|
mpl_toolkits/tests/test_axisartist_axis_artist.py,sha256=h8UXVxnt-fsfvjEOLxnyrwA4z0b7Lf-1UtvIh_SNaI4,2893
|
||||||
|
mpl_toolkits/tests/test_axisartist_axislines.py,sha256=nzxykaFzR1XaQ7KlJNf9VNDXoXRQY4BZKuH_iIyiCHk,2266
|
||||||
|
mpl_toolkits/tests/test_axisartist_clip_path.py,sha256=7K1Y-2DPbDdyvpAHq3XEDaXfsikw-u8v7olwaqwZ53o,1054
|
||||||
|
mpl_toolkits/tests/test_axisartist_floating_axes.py,sha256=bTaH-fTMJum4DAjH5h4t-hH3W5BNNw9LuiWPaC24j6Q,4165
|
||||||
|
mpl_toolkits/tests/test_axisartist_grid_finder.py,sha256=e65sLudWFIXeU08Sis3_SI1JEI6eq8YqKj-80F_Nohk,325
|
||||||
|
mpl_toolkits/tests/test_axisartist_grid_helper_curvelinear.py,sha256=TpV3ShQOPAk57KV-97dWfpULRws0zyAOQwn3JpejTe4,7487
|
||||||
|
mpl_toolkits/tests/test_mplot3d.py,sha256=BUvMUV5OkJskzLZ1KBIIFgKIWoiWaw38FOYBJT0_xRM,26306
|
||||||
|
pylab.py,sha256=u_By3CHla-rBMg57egFXIxZ3P_J6zEkSu_dNpBcH5pw,90
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Wheel-Version: 1.0
|
||||||
|
Generator: bdist_wheel (0.31.1)
|
||||||
|
Root-Is-Purelib: false
|
||||||
|
Tag: cp36-cp36m-manylinux1_x86_64
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
mpl_toolkits
|
||||||
|
mpl_toolkits
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
matplotlib
|
||||||
|
mpl_toolkits
|
||||||
|
pylab
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
# Javascript template for HTMLWriter
|
||||||
|
JS_INCLUDE = """
|
||||||
|
<link rel="stylesheet"
|
||||||
|
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/
|
||||||
|
css/font-awesome.min.css">
|
||||||
|
<script language="javascript">
|
||||||
|
/* Define the Animation class */
|
||||||
|
function Animation(frames, img_id, slider_id, interval, loop_select_id){
|
||||||
|
this.img_id = img_id;
|
||||||
|
this.slider_id = slider_id;
|
||||||
|
this.loop_select_id = loop_select_id;
|
||||||
|
this.interval = interval;
|
||||||
|
this.current_frame = 0;
|
||||||
|
this.direction = 0;
|
||||||
|
this.timer = null;
|
||||||
|
this.frames = new Array(frames.length);
|
||||||
|
|
||||||
|
for (var i=0; i<frames.length; i++)
|
||||||
|
{
|
||||||
|
this.frames[i] = new Image();
|
||||||
|
this.frames[i].src = frames[i];
|
||||||
|
}
|
||||||
|
document.getElementById(this.slider_id).max = this.frames.length - 1;
|
||||||
|
this.set_frame(this.current_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.get_loop_state = function(){
|
||||||
|
var button_group = document[this.loop_select_id].state;
|
||||||
|
for (var i = 0; i < button_group.length; i++) {
|
||||||
|
var button = button_group[i];
|
||||||
|
if (button.checked) {
|
||||||
|
return button.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.set_frame = function(frame){
|
||||||
|
this.current_frame = frame;
|
||||||
|
document.getElementById(this.img_id).src =
|
||||||
|
this.frames[this.current_frame].src;
|
||||||
|
document.getElementById(this.slider_id).value = this.current_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.next_frame = function()
|
||||||
|
{
|
||||||
|
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.previous_frame = function()
|
||||||
|
{
|
||||||
|
this.set_frame(Math.max(0, this.current_frame - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.first_frame = function()
|
||||||
|
{
|
||||||
|
this.set_frame(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.last_frame = function()
|
||||||
|
{
|
||||||
|
this.set_frame(this.frames.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.slower = function()
|
||||||
|
{
|
||||||
|
this.interval /= 0.7;
|
||||||
|
if(this.direction > 0){this.play_animation();}
|
||||||
|
else if(this.direction < 0){this.reverse_animation();}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.faster = function()
|
||||||
|
{
|
||||||
|
this.interval *= 0.7;
|
||||||
|
if(this.direction > 0){this.play_animation();}
|
||||||
|
else if(this.direction < 0){this.reverse_animation();}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.anim_step_forward = function()
|
||||||
|
{
|
||||||
|
this.current_frame += 1;
|
||||||
|
if(this.current_frame < this.frames.length){
|
||||||
|
this.set_frame(this.current_frame);
|
||||||
|
}else{
|
||||||
|
var loop_state = this.get_loop_state();
|
||||||
|
if(loop_state == "loop"){
|
||||||
|
this.first_frame();
|
||||||
|
}else if(loop_state == "reflect"){
|
||||||
|
this.last_frame();
|
||||||
|
this.reverse_animation();
|
||||||
|
}else{
|
||||||
|
this.pause_animation();
|
||||||
|
this.last_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.anim_step_reverse = function()
|
||||||
|
{
|
||||||
|
this.current_frame -= 1;
|
||||||
|
if(this.current_frame >= 0){
|
||||||
|
this.set_frame(this.current_frame);
|
||||||
|
}else{
|
||||||
|
var loop_state = this.get_loop_state();
|
||||||
|
if(loop_state == "loop"){
|
||||||
|
this.last_frame();
|
||||||
|
}else if(loop_state == "reflect"){
|
||||||
|
this.first_frame();
|
||||||
|
this.play_animation();
|
||||||
|
}else{
|
||||||
|
this.pause_animation();
|
||||||
|
this.first_frame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.pause_animation = function()
|
||||||
|
{
|
||||||
|
this.direction = 0;
|
||||||
|
if (this.timer){
|
||||||
|
clearInterval(this.timer);
|
||||||
|
this.timer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.play_animation = function()
|
||||||
|
{
|
||||||
|
this.pause_animation();
|
||||||
|
this.direction = 1;
|
||||||
|
var t = this;
|
||||||
|
if (!this.timer) this.timer = setInterval(function() {
|
||||||
|
t.anim_step_forward();
|
||||||
|
}, this.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation.prototype.reverse_animation = function()
|
||||||
|
{
|
||||||
|
this.pause_animation();
|
||||||
|
this.direction = -1;
|
||||||
|
var t = this;
|
||||||
|
if (!this.timer) this.timer = setInterval(function() {
|
||||||
|
t.anim_step_reverse();
|
||||||
|
}, this.interval);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# HTML template for HTMLWriter
|
||||||
|
DISPLAY_TEMPLATE = """
|
||||||
|
<div class="animation" align="center">
|
||||||
|
<img id="_anim_img{id}">
|
||||||
|
<br>
|
||||||
|
<input id="_anim_slider{id}" type="range" style="width:350px"
|
||||||
|
name="points" min="0" max="1" step="1" value="0"
|
||||||
|
onchange="anim{id}.set_frame(parseInt(this.value));"></input>
|
||||||
|
<br>
|
||||||
|
<button onclick="anim{id}.slower()"><i class="fa fa-minus"></i></button>
|
||||||
|
<button onclick="anim{id}.first_frame()"><i class="fa fa-fast-backward">
|
||||||
|
</i></button>
|
||||||
|
<button onclick="anim{id}.previous_frame()">
|
||||||
|
<i class="fa fa-step-backward"></i></button>
|
||||||
|
<button onclick="anim{id}.reverse_animation()">
|
||||||
|
<i class="fa fa-play fa-flip-horizontal"></i></button>
|
||||||
|
<button onclick="anim{id}.pause_animation()"><i class="fa fa-pause">
|
||||||
|
</i></button>
|
||||||
|
<button onclick="anim{id}.play_animation()"><i class="fa fa-play"></i>
|
||||||
|
</button>
|
||||||
|
<button onclick="anim{id}.next_frame()"><i class="fa fa-step-forward">
|
||||||
|
</i></button>
|
||||||
|
<button onclick="anim{id}.last_frame()"><i class="fa fa-fast-forward">
|
||||||
|
</i></button>
|
||||||
|
<button onclick="anim{id}.faster()"><i class="fa fa-plus"></i></button>
|
||||||
|
<form action="#n" name="_anim_loop_select{id}" class="anim_control">
|
||||||
|
<input type="radio" name="state"
|
||||||
|
value="once" {once_checked}> Once </input>
|
||||||
|
<input type="radio" name="state"
|
||||||
|
value="loop" {loop_checked}> Loop </input>
|
||||||
|
<input type="radio" name="state"
|
||||||
|
value="reflect" {reflect_checked}> Reflect </input>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script language="javascript">
|
||||||
|
/* Instantiate the Animation class. */
|
||||||
|
/* The IDs given should match those used in the template above. */
|
||||||
|
(function() {{
|
||||||
|
var img_id = "_anim_img{id}";
|
||||||
|
var slider_id = "_anim_slider{id}";
|
||||||
|
var loop_select_id = "_anim_loop_select{id}";
|
||||||
|
var frames = new Array({Nframes});
|
||||||
|
{fill_frames}
|
||||||
|
|
||||||
|
/* set a timeout to make sure all the above elements are created before
|
||||||
|
the object is initialized. */
|
||||||
|
setTimeout(function() {{
|
||||||
|
anim{id} = new Animation(frames, img_id, slider_id, {interval},
|
||||||
|
loop_select_id);
|
||||||
|
}}, 0);
|
||||||
|
}})()
|
||||||
|
</script>
|
||||||
|
"""
|
||||||
|
|
||||||
|
INCLUDED_FRAMES = """
|
||||||
|
for (var i=0; i<{Nframes}; i++){{
|
||||||
|
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
|
||||||
|
".{frame_format}";
|
||||||
|
}}
|
||||||
|
"""
|
||||||
@@ -0,0 +1,729 @@
|
|||||||
|
"""
|
||||||
|
This module provides the routine to adjust subplot layouts so that there are
|
||||||
|
no overlapping axes or axes decorations. All axes decorations are dealt with
|
||||||
|
(labels, ticks, titles, ticklabels) and some dependent artists are also dealt
|
||||||
|
with (colorbar, suptitle, legend).
|
||||||
|
|
||||||
|
Layout is done via :meth:`~matplotlib.gridspec`, with one constraint per
|
||||||
|
gridspec, so it is possible to have overlapping axes if the gridspecs
|
||||||
|
overlap (i.e. using :meth:`~matplotlib.gridspec.GridSpecFromSubplotSpec`).
|
||||||
|
Axes placed using ``figure.subplots()`` or ``figure.add_subplots()`` will
|
||||||
|
participate in the layout. Axes manually placed via ``figure.add_axes()``
|
||||||
|
will not.
|
||||||
|
|
||||||
|
See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Development Notes:
|
||||||
|
|
||||||
|
# What gets a layoutbox:
|
||||||
|
# - figure
|
||||||
|
# - gridspec
|
||||||
|
# - subplotspec
|
||||||
|
# EITHER:
|
||||||
|
# - axes + pos for the axes (i.e. the total area taken by axis and
|
||||||
|
# the actual "position" argument that needs to be sent to
|
||||||
|
# ax.set_position.)
|
||||||
|
# - The axes layout box will also encomapss the legend, and that is
|
||||||
|
# how legends get included (axes legeneds, not figure legends)
|
||||||
|
# - colorbars are sibblings of the axes if they are single-axes
|
||||||
|
# colorbars
|
||||||
|
# OR:
|
||||||
|
# - a gridspec can be inside a subplotspec.
|
||||||
|
# - subplotspec
|
||||||
|
# EITHER:
|
||||||
|
# - axes...
|
||||||
|
# OR:
|
||||||
|
# - gridspec... with arbitrary nesting...
|
||||||
|
# - colorbars are siblings of the subplotspecs if they are multi-axes
|
||||||
|
# colorbars.
|
||||||
|
# - suptitle:
|
||||||
|
# - right now suptitles are just stacked atop everything else in figure.
|
||||||
|
# Could imagine suptitles being gridspec suptitles, but not implimented
|
||||||
|
#
|
||||||
|
# Todo: AnchoredOffsetbox connected to gridspecs or axes. This would
|
||||||
|
# be more general way to add extra-axes annotations.
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from matplotlib.legend import Legend
|
||||||
|
import matplotlib.transforms as transforms
|
||||||
|
import matplotlib._layoutbox as layoutbox
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def _in_same_column(colnum0min, colnum0max, colnumCmin, colnumCmax):
|
||||||
|
return (colnumCmin <= colnum0min <= colnumCmax
|
||||||
|
or colnumCmin <= colnum0max <= colnumCmax)
|
||||||
|
|
||||||
|
|
||||||
|
def _in_same_row(rownum0min, rownum0max, rownumCmin, rownumCmax):
|
||||||
|
return (rownumCmin <= rownum0min <= rownumCmax
|
||||||
|
or rownumCmin <= rownum0max <= rownumCmax)
|
||||||
|
|
||||||
|
|
||||||
|
def _axes_all_finite_sized(fig):
|
||||||
|
"""
|
||||||
|
helper function to make sure all axes in the
|
||||||
|
figure have a finite width and height. If not, return False
|
||||||
|
"""
|
||||||
|
for ax in fig.axes:
|
||||||
|
if ax._layoutbox is not None:
|
||||||
|
newpos = ax._poslayoutbox.get_rect()
|
||||||
|
if newpos[2] <= 0 or newpos[3] <= 0:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
######################################################
|
||||||
|
def do_constrained_layout(fig, renderer, h_pad, w_pad,
|
||||||
|
hspace=None, wspace=None):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Do the constrained_layout. Called at draw time in
|
||||||
|
``figure.constrained_layout()``
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
|
||||||
|
fig: Figure
|
||||||
|
is the ``figure`` instance to do the layout in.
|
||||||
|
|
||||||
|
renderer: Renderer
|
||||||
|
the renderer to use.
|
||||||
|
|
||||||
|
h_pad, w_pad : float
|
||||||
|
are in figure-normalized units, and are a padding around the axes
|
||||||
|
elements.
|
||||||
|
|
||||||
|
hspace, wspace : float
|
||||||
|
are in fractions of the subplot sizes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
''' Steps:
|
||||||
|
|
||||||
|
1. get a list of unique gridspecs in this figure. Each gridspec will be
|
||||||
|
constrained separately.
|
||||||
|
2. Check for gaps in the gridspecs. i.e. if not every axes slot in the
|
||||||
|
gridspec has been filled. If empty, add a ghost axis that is made so
|
||||||
|
that it cannot be seen (though visible=True). This is needed to make
|
||||||
|
a blank spot in the layout.
|
||||||
|
3. Compare the tight_bbox of each axes to its `position`, and assume that
|
||||||
|
the difference is the space needed by the elements around the edge of
|
||||||
|
the axes (decorations) like the title, ticklabels, x-labels, etc. This
|
||||||
|
can include legends who overspill the axes boundaries.
|
||||||
|
4. Constrain gridspec elements to line up:
|
||||||
|
a) if colnum0 neq colnumC, the two subplotspecs are stacked next to
|
||||||
|
each other, with the appropriate order.
|
||||||
|
b) if colnum0 == columnC line up the left or right side of the
|
||||||
|
_poslayoutbox (depending if it is the min or max num that is equal).
|
||||||
|
c) do the same for rows...
|
||||||
|
5. The above doesn't constrain relative sizes of the _poslayoutboxes at
|
||||||
|
all, and indeed zero-size is a solution that the solver often finds more
|
||||||
|
convenient than expanding the sizes. Right now the solution is to compare
|
||||||
|
subplotspec sizes (i.e. drowsC and drows0) and constrain the larger
|
||||||
|
_poslayoutbox to be larger than the ratio of the sizes. i.e. if drows0 >
|
||||||
|
drowsC, then ax._poslayoutbox > axc._poslayoutbox * drowsC / drows0. This
|
||||||
|
works fine *if* the decorations are similar between the axes. If the
|
||||||
|
larger subplotspec has much larger axes decorations, then the constraint
|
||||||
|
above is incorrect.
|
||||||
|
|
||||||
|
We need the greater than in the above, in general, rather than an equals
|
||||||
|
sign. Consider the case of the left column having 2 rows, and the right
|
||||||
|
column having 1 row. We want the top and bottom of the _poslayoutboxes to
|
||||||
|
line up. So that means if there are decorations on the left column axes
|
||||||
|
they will be smaller than half as large as the right hand axis.
|
||||||
|
|
||||||
|
This can break down if the decoration size for the right hand axis (the
|
||||||
|
margins) is very large. There must be a math way to check for this case.
|
||||||
|
|
||||||
|
'''
|
||||||
|
|
||||||
|
invTransFig = fig.transFigure.inverted().transform_bbox
|
||||||
|
|
||||||
|
# list of unique gridspecs that contain child axes:
|
||||||
|
gss = set()
|
||||||
|
for ax in fig.axes:
|
||||||
|
if hasattr(ax, 'get_subplotspec'):
|
||||||
|
gs = ax.get_subplotspec().get_gridspec()
|
||||||
|
if gs._layoutbox is not None:
|
||||||
|
gss.add(gs)
|
||||||
|
if len(gss) == 0:
|
||||||
|
warnings.warn('There are no gridspecs with layoutboxes. '
|
||||||
|
'Possibly did not call parent GridSpec with the figure= '
|
||||||
|
'keyword')
|
||||||
|
|
||||||
|
if fig._layoutbox.constrained_layout_called < 1:
|
||||||
|
for gs in gss:
|
||||||
|
# fill in any empty gridspec slots w/ ghost axes...
|
||||||
|
_make_ghost_gridspec_slots(fig, gs)
|
||||||
|
|
||||||
|
for nnn in range(2):
|
||||||
|
# do the algrithm twice. This has to be done because decorators
|
||||||
|
# change size after the first re-position (i.e. x/yticklabels get
|
||||||
|
# larger/smaller). This second reposition tends to be much milder,
|
||||||
|
# so doing twice makes things work OK.
|
||||||
|
for ax in fig.axes:
|
||||||
|
_log.debug(ax._layoutbox)
|
||||||
|
if ax._layoutbox is not None:
|
||||||
|
# make margins for each layout box based on the size of
|
||||||
|
# the decorators.
|
||||||
|
_make_layout_margins(ax, renderer, h_pad, w_pad)
|
||||||
|
|
||||||
|
# do layout for suptitle.
|
||||||
|
if fig._suptitle is not None and fig._suptitle._layoutbox is not None:
|
||||||
|
sup = fig._suptitle
|
||||||
|
bbox = invTransFig(sup.get_window_extent(renderer=renderer))
|
||||||
|
height = bbox.y1 - bbox.y0
|
||||||
|
sup._layoutbox.edit_height(height+h_pad)
|
||||||
|
|
||||||
|
# OK, the above lines up ax._poslayoutbox with ax._layoutbox
|
||||||
|
# now we need to
|
||||||
|
# 1) arrange the subplotspecs. We do it at this level because
|
||||||
|
# the subplotspecs are meant to contain other dependent axes
|
||||||
|
# like colorbars or legends.
|
||||||
|
# 2) line up the right and left side of the ax._poslayoutbox
|
||||||
|
# that have the same subplotspec maxes.
|
||||||
|
|
||||||
|
if fig._layoutbox.constrained_layout_called < 1:
|
||||||
|
# arrange the subplotspecs... This is all done relative to each
|
||||||
|
# other. Some subplotspecs conatain axes, and others contain
|
||||||
|
# gridspecs the ones that contain gridspecs are a set proportion
|
||||||
|
# of their parent gridspec. The ones that contain axes are
|
||||||
|
# not so constrained.
|
||||||
|
figlb = fig._layoutbox
|
||||||
|
for child in figlb.children:
|
||||||
|
if child._is_gridspec_layoutbox():
|
||||||
|
# This routine makes all the subplot spec containers
|
||||||
|
# have the correct arrangement. It just stacks the
|
||||||
|
# subplot layoutboxes in the correct order...
|
||||||
|
_arrange_subplotspecs(child, hspace=hspace, wspace=wspace)
|
||||||
|
|
||||||
|
for gs in gss:
|
||||||
|
_align_spines(fig, gs)
|
||||||
|
|
||||||
|
fig._layoutbox.constrained_layout_called += 1
|
||||||
|
fig._layoutbox.update_variables()
|
||||||
|
|
||||||
|
# check if any axes collapsed to zero. If not, don't change positions:
|
||||||
|
if _axes_all_finite_sized(fig):
|
||||||
|
# Now set the position of the axes...
|
||||||
|
for ax in fig.axes:
|
||||||
|
if ax._layoutbox is not None:
|
||||||
|
newpos = ax._poslayoutbox.get_rect()
|
||||||
|
# Now set the new position.
|
||||||
|
# ax.set_position will zero out the layout for
|
||||||
|
# this axis, allowing users to hard-code the position,
|
||||||
|
# so this does the same w/o zeroing layout.
|
||||||
|
ax._set_position(newpos, which='original')
|
||||||
|
else:
|
||||||
|
warnings.warn('constrained_layout not applied. At least '
|
||||||
|
'one axes collapsed to zero width or height.')
|
||||||
|
|
||||||
|
|
||||||
|
def _make_ghost_gridspec_slots(fig, gs):
|
||||||
|
"""
|
||||||
|
Check for unoccupied gridspec slots and make ghost axes for these
|
||||||
|
slots... Do for each gs separately. This is a pretty big kludge
|
||||||
|
but shoudn't have too much ill effect. The worst is that
|
||||||
|
someone querrying the figure will wonder why there are more
|
||||||
|
axes than they thought.
|
||||||
|
"""
|
||||||
|
nrows, ncols = gs.get_geometry()
|
||||||
|
hassubplotspec = np.zeros(nrows * ncols, dtype=bool)
|
||||||
|
axs = []
|
||||||
|
for ax in fig.axes:
|
||||||
|
if (hasattr(ax, 'get_subplotspec')
|
||||||
|
and ax._layoutbox is not None
|
||||||
|
and ax.get_subplotspec().get_gridspec() == gs):
|
||||||
|
axs += [ax]
|
||||||
|
for ax in axs:
|
||||||
|
ss0 = ax.get_subplotspec()
|
||||||
|
if ss0.num2 is None:
|
||||||
|
ss0.num2 = ss0.num1
|
||||||
|
hassubplotspec[ss0.num1:(ss0.num2 + 1)] = True
|
||||||
|
for nn, hss in enumerate(hassubplotspec):
|
||||||
|
if not hss:
|
||||||
|
# this gridspec slot doesn't have an axis so we
|
||||||
|
# make a "ghost".
|
||||||
|
ax = fig.add_subplot(gs[nn])
|
||||||
|
ax.set_frame_on(False)
|
||||||
|
ax.set_xticks([])
|
||||||
|
ax.set_yticks([])
|
||||||
|
ax.set_facecolor((1, 0, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
|
def _make_layout_margins(ax, renderer, h_pad, w_pad):
|
||||||
|
"""
|
||||||
|
For each axes, make a margin between the *pos* layoutbox and the
|
||||||
|
*axes* layoutbox be a minimum size that can accommodate the
|
||||||
|
decorations on the axis.
|
||||||
|
"""
|
||||||
|
fig = ax.figure
|
||||||
|
invTransFig = fig.transFigure.inverted().transform_bbox
|
||||||
|
|
||||||
|
pos = ax.get_position(original=True)
|
||||||
|
tightbbox = ax.get_tightbbox(renderer=renderer)
|
||||||
|
bbox = invTransFig(tightbbox)
|
||||||
|
# use stored h_pad if it exists
|
||||||
|
h_padt = ax._poslayoutbox.h_pad
|
||||||
|
if h_padt is None:
|
||||||
|
h_padt = h_pad
|
||||||
|
w_padt = ax._poslayoutbox.w_pad
|
||||||
|
if w_padt is None:
|
||||||
|
w_padt = w_pad
|
||||||
|
ax._poslayoutbox.edit_left_margin_min(-bbox.x0 +
|
||||||
|
pos.x0 + w_padt)
|
||||||
|
ax._poslayoutbox.edit_right_margin_min(bbox.x1 -
|
||||||
|
pos.x1 + w_padt)
|
||||||
|
ax._poslayoutbox.edit_bottom_margin_min(
|
||||||
|
-bbox.y0 + pos.y0 + h_padt)
|
||||||
|
ax._poslayoutbox.edit_top_margin_min(bbox.y1-pos.y1+h_padt)
|
||||||
|
_log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad))
|
||||||
|
_log.debug('right %f', (bbox.x1 - pos.x1 + w_pad))
|
||||||
|
_log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt))
|
||||||
|
# Sometimes its possible for the solver to collapse
|
||||||
|
# rather than expand axes, so they all have zero height
|
||||||
|
# or width. This stops that... It *should* have been
|
||||||
|
# taken into account w/ pref_width...
|
||||||
|
if fig._layoutbox.constrained_layout_called < 1:
|
||||||
|
ax._poslayoutbox.constrain_height_min(20, strength='weak')
|
||||||
|
ax._poslayoutbox.constrain_width_min(20, strength='weak')
|
||||||
|
ax._layoutbox.constrain_height_min(20, strength='weak')
|
||||||
|
ax._layoutbox.constrain_width_min(20, strength='weak')
|
||||||
|
ax._poslayoutbox.constrain_top_margin(0, strength='weak')
|
||||||
|
ax._poslayoutbox.constrain_bottom_margin(0,
|
||||||
|
strength='weak')
|
||||||
|
ax._poslayoutbox.constrain_right_margin(0, strength='weak')
|
||||||
|
ax._poslayoutbox.constrain_left_margin(0, strength='weak')
|
||||||
|
|
||||||
|
|
||||||
|
def _align_spines(fig, gs):
|
||||||
|
"""
|
||||||
|
- Align right/left and bottom/top spines of appropriate subplots.
|
||||||
|
- Compare size of subplotspec including height and width ratios
|
||||||
|
and make sure that the axes spines are at least as large
|
||||||
|
as they should be.
|
||||||
|
"""
|
||||||
|
# for each gridspec...
|
||||||
|
nrows, ncols = gs.get_geometry()
|
||||||
|
width_ratios = gs.get_width_ratios()
|
||||||
|
height_ratios = gs.get_height_ratios()
|
||||||
|
if width_ratios is None:
|
||||||
|
width_ratios = np.ones(ncols)
|
||||||
|
if height_ratios is None:
|
||||||
|
height_ratios = np.ones(nrows)
|
||||||
|
|
||||||
|
# get axes in this gridspec....
|
||||||
|
axs = []
|
||||||
|
for ax in fig.axes:
|
||||||
|
if (hasattr(ax, 'get_subplotspec')
|
||||||
|
and ax._layoutbox is not None):
|
||||||
|
if ax.get_subplotspec().get_gridspec() == gs:
|
||||||
|
axs += [ax]
|
||||||
|
rownummin = np.zeros(len(axs), dtype=np.int8)
|
||||||
|
rownummax = np.zeros(len(axs), dtype=np.int8)
|
||||||
|
colnummin = np.zeros(len(axs), dtype=np.int8)
|
||||||
|
colnummax = np.zeros(len(axs), dtype=np.int8)
|
||||||
|
width = np.zeros(len(axs))
|
||||||
|
height = np.zeros(len(axs))
|
||||||
|
|
||||||
|
for n, ax in enumerate(axs):
|
||||||
|
ss0 = ax.get_subplotspec()
|
||||||
|
if ss0.num2 is None:
|
||||||
|
ss0.num2 = ss0.num1
|
||||||
|
rownummin[n], colnummin[n] = divmod(ss0.num1, ncols)
|
||||||
|
rownummax[n], colnummax[n] = divmod(ss0.num2, ncols)
|
||||||
|
width[n] = np.sum(
|
||||||
|
width_ratios[colnummin[n]:(colnummax[n] + 1)])
|
||||||
|
height[n] = np.sum(
|
||||||
|
height_ratios[rownummin[n]:(rownummax[n] + 1)])
|
||||||
|
|
||||||
|
for nn, ax in enumerate(axs[:-1]):
|
||||||
|
ss0 = ax.get_subplotspec()
|
||||||
|
|
||||||
|
# now compare ax to all the axs:
|
||||||
|
#
|
||||||
|
# If the subplotspecs have the same colnumXmax, then line
|
||||||
|
# up their right sides. If they have the same min, then
|
||||||
|
# line up their left sides (and vertical equivalents).
|
||||||
|
rownum0min, colnum0min = rownummin[nn], colnummin[nn]
|
||||||
|
rownum0max, colnum0max = rownummax[nn], colnummax[nn]
|
||||||
|
width0, height0 = width[nn], height[nn]
|
||||||
|
alignleft = False
|
||||||
|
alignright = False
|
||||||
|
alignbot = False
|
||||||
|
aligntop = False
|
||||||
|
alignheight = False
|
||||||
|
alignwidth = False
|
||||||
|
for mm in range(nn+1, len(axs)):
|
||||||
|
axc = axs[mm]
|
||||||
|
rownumCmin, colnumCmin = rownummin[mm], colnummin[mm]
|
||||||
|
rownumCmax, colnumCmax = rownummax[mm], colnummax[mm]
|
||||||
|
widthC, heightC = width[mm], height[mm]
|
||||||
|
# Horizontally align axes spines if they have the
|
||||||
|
# same min or max:
|
||||||
|
if not alignleft and colnum0min == colnumCmin:
|
||||||
|
# we want the _poslayoutboxes to line up on left
|
||||||
|
# side of the axes spines...
|
||||||
|
layoutbox.align([ax._poslayoutbox,
|
||||||
|
axc._poslayoutbox],
|
||||||
|
'left')
|
||||||
|
alignleft = True
|
||||||
|
|
||||||
|
if not alignright and colnum0max == colnumCmax:
|
||||||
|
# line up right sides of _poslayoutbox
|
||||||
|
layoutbox.align([ax._poslayoutbox,
|
||||||
|
axc._poslayoutbox],
|
||||||
|
'right')
|
||||||
|
alignright = True
|
||||||
|
# Vertically align axes spines if they have the
|
||||||
|
# same min or max:
|
||||||
|
if not aligntop and rownum0min == rownumCmin:
|
||||||
|
# line up top of _poslayoutbox
|
||||||
|
_log.debug('rownum0min == rownumCmin')
|
||||||
|
layoutbox.align([ax._poslayoutbox, axc._poslayoutbox],
|
||||||
|
'top')
|
||||||
|
aligntop = True
|
||||||
|
|
||||||
|
if not alignbot and rownum0max == rownumCmax:
|
||||||
|
# line up bottom of _poslayoutbox
|
||||||
|
_log.debug('rownum0max == rownumCmax')
|
||||||
|
layoutbox.align([ax._poslayoutbox, axc._poslayoutbox],
|
||||||
|
'bottom')
|
||||||
|
alignbot = True
|
||||||
|
###########
|
||||||
|
# Now we make the widths and heights of position boxes
|
||||||
|
# similar. (i.e the spine locations)
|
||||||
|
# This allows vertically stacked subplots to have
|
||||||
|
# different sizes if they occupy different amounts
|
||||||
|
# of the gridspec: i.e.
|
||||||
|
# gs = gridspec.GridSpec(3,1)
|
||||||
|
# ax1 = gs[0,:]
|
||||||
|
# ax2 = gs[1:,:]
|
||||||
|
# then drows0 = 1, and drowsC = 2, and ax2
|
||||||
|
# should be at least twice as large as ax1.
|
||||||
|
# But it can be more than twice as large because
|
||||||
|
# it needs less room for the labeling.
|
||||||
|
#
|
||||||
|
# For height, this only needs to be done if the
|
||||||
|
# subplots share a column. For width if they
|
||||||
|
# share a row.
|
||||||
|
|
||||||
|
drowsC = (rownumCmax - rownumCmin + 1)
|
||||||
|
drows0 = (rownum0max - rownum0min + 1)
|
||||||
|
dcolsC = (colnumCmax - colnumCmin + 1)
|
||||||
|
dcols0 = (colnum0max - colnum0min + 1)
|
||||||
|
|
||||||
|
if not alignheight and drows0 == drowsC:
|
||||||
|
ax._poslayoutbox.constrain_height(
|
||||||
|
axc._poslayoutbox.height * height0 / heightC)
|
||||||
|
alignheight = True
|
||||||
|
elif _in_same_column(colnum0min, colnum0max,
|
||||||
|
colnumCmin, colnumCmax):
|
||||||
|
if height0 > heightC:
|
||||||
|
ax._poslayoutbox.constrain_height_min(
|
||||||
|
axc._poslayoutbox.height * height0 / heightC)
|
||||||
|
# these constraints stop the smaller axes from
|
||||||
|
# being allowed to go to zero height...
|
||||||
|
axc._poslayoutbox.constrain_height_min(
|
||||||
|
ax._poslayoutbox.height * heightC /
|
||||||
|
(height0*1.8))
|
||||||
|
elif height0 < heightC:
|
||||||
|
axc._poslayoutbox.constrain_height_min(
|
||||||
|
ax._poslayoutbox.height * heightC / height0)
|
||||||
|
ax._poslayoutbox.constrain_height_min(
|
||||||
|
ax._poslayoutbox.height * height0 /
|
||||||
|
(heightC*1.8))
|
||||||
|
# widths...
|
||||||
|
if not alignwidth and dcols0 == dcolsC:
|
||||||
|
ax._poslayoutbox.constrain_width(
|
||||||
|
axc._poslayoutbox.width * width0 / widthC)
|
||||||
|
alignwidth = True
|
||||||
|
elif _in_same_row(rownum0min, rownum0max,
|
||||||
|
rownumCmin, rownumCmax):
|
||||||
|
if width0 > widthC:
|
||||||
|
ax._poslayoutbox.constrain_width_min(
|
||||||
|
axc._poslayoutbox.width * width0 / widthC)
|
||||||
|
axc._poslayoutbox.constrain_width_min(
|
||||||
|
ax._poslayoutbox.width * widthC /
|
||||||
|
(width0*1.8))
|
||||||
|
elif width0 < widthC:
|
||||||
|
axc._poslayoutbox.constrain_width_min(
|
||||||
|
ax._poslayoutbox.width * widthC / width0)
|
||||||
|
ax._poslayoutbox.constrain_width_min(
|
||||||
|
axc._poslayoutbox.width * width0 /
|
||||||
|
(widthC*1.8))
|
||||||
|
|
||||||
|
|
||||||
|
def _arrange_subplotspecs(gs, hspace=0, wspace=0):
|
||||||
|
"""
|
||||||
|
arrange the subplotspec children of this gridspec, and then recursively
|
||||||
|
do the same of any gridspec children of those gridspecs...
|
||||||
|
"""
|
||||||
|
sschildren = []
|
||||||
|
for child in gs.children:
|
||||||
|
if child._is_subplotspec_layoutbox():
|
||||||
|
for child2 in child.children:
|
||||||
|
# check for gridspec children...
|
||||||
|
if child2._is_gridspec_layoutbox():
|
||||||
|
_arrange_subplotspecs(child2, hspace=hspace, wspace=wspace)
|
||||||
|
sschildren += [child]
|
||||||
|
# now arrange the subplots...
|
||||||
|
for child0 in sschildren:
|
||||||
|
ss0 = child0.artist
|
||||||
|
nrows, ncols = ss0.get_gridspec().get_geometry()
|
||||||
|
if ss0.num2 is None:
|
||||||
|
ss0.num2 = ss0.num1
|
||||||
|
rowNum0min, colNum0min = divmod(ss0.num1, ncols)
|
||||||
|
rowNum0max, colNum0max = divmod(ss0.num2, ncols)
|
||||||
|
sschildren = sschildren[1:]
|
||||||
|
for childc in sschildren:
|
||||||
|
ssc = childc.artist
|
||||||
|
rowNumCmin, colNumCmin = divmod(ssc.num1, ncols)
|
||||||
|
if ssc.num2 is None:
|
||||||
|
ssc.num2 = ssc.num1
|
||||||
|
rowNumCmax, colNumCmax = divmod(ssc.num2, ncols)
|
||||||
|
# OK, this tells us the relative layout of ax
|
||||||
|
# with axc
|
||||||
|
thepad = wspace / ncols
|
||||||
|
if colNum0max < colNumCmin:
|
||||||
|
layoutbox.hstack([ss0._layoutbox, ssc._layoutbox],
|
||||||
|
padding=thepad)
|
||||||
|
if colNumCmax < colNum0min:
|
||||||
|
layoutbox.hstack([ssc._layoutbox, ss0._layoutbox],
|
||||||
|
padding=thepad)
|
||||||
|
|
||||||
|
####
|
||||||
|
# vertical alignment
|
||||||
|
thepad = hspace / nrows
|
||||||
|
if rowNum0max < rowNumCmin:
|
||||||
|
layoutbox.vstack([ss0._layoutbox,
|
||||||
|
ssc._layoutbox],
|
||||||
|
padding=thepad)
|
||||||
|
if rowNumCmax < rowNum0min:
|
||||||
|
layoutbox.vstack([ssc._layoutbox,
|
||||||
|
ss0._layoutbox],
|
||||||
|
padding=thepad)
|
||||||
|
|
||||||
|
|
||||||
|
def layoutcolorbarsingle(ax, cax, shrink, aspect, location, pad=0.05):
|
||||||
|
"""
|
||||||
|
Do the layout for a colorbar, to not oeverly pollute colorbar.py
|
||||||
|
|
||||||
|
`pad` is in fraction of the original axis size.
|
||||||
|
"""
|
||||||
|
axlb = ax._layoutbox
|
||||||
|
axpos = ax._poslayoutbox
|
||||||
|
axsslb = ax.get_subplotspec()._layoutbox
|
||||||
|
lb = layoutbox.LayoutBox(
|
||||||
|
parent=axsslb,
|
||||||
|
name=axsslb.name + '.cbar',
|
||||||
|
artist=cax)
|
||||||
|
|
||||||
|
if location in ('left', 'right'):
|
||||||
|
lbpos = layoutbox.LayoutBox(
|
||||||
|
parent=lb,
|
||||||
|
name=lb.name + '.pos',
|
||||||
|
tightwidth=False,
|
||||||
|
pos=True,
|
||||||
|
subplot=False,
|
||||||
|
artist=cax)
|
||||||
|
|
||||||
|
if location == 'right':
|
||||||
|
# arrange to right of parent axis
|
||||||
|
layoutbox.hstack([axlb, lb], padding=pad * axlb.width,
|
||||||
|
strength='strong')
|
||||||
|
else:
|
||||||
|
layoutbox.hstack([lb, axlb], padding=pad * axlb.width)
|
||||||
|
# constrain the height and center...
|
||||||
|
layoutbox.match_heights([axpos, lbpos], [1, shrink])
|
||||||
|
layoutbox.align([axpos, lbpos], 'v_center')
|
||||||
|
# set the width of the pos box
|
||||||
|
lbpos.constrain_width(shrink * axpos.height * (1/aspect),
|
||||||
|
strength='strong')
|
||||||
|
elif location in ('bottom', 'top'):
|
||||||
|
lbpos = layoutbox.LayoutBox(
|
||||||
|
parent=lb,
|
||||||
|
name=lb.name + '.pos',
|
||||||
|
tightheight=True,
|
||||||
|
pos=True,
|
||||||
|
subplot=False,
|
||||||
|
artist=cax)
|
||||||
|
|
||||||
|
if location == 'bottom':
|
||||||
|
layoutbox.vstack([axlb, lb], padding=pad * axlb.height)
|
||||||
|
else:
|
||||||
|
layoutbox.vstack([lb, axlb], padding=pad * axlb.height)
|
||||||
|
# constrain the height and center...
|
||||||
|
layoutbox.match_widths([axpos, lbpos],
|
||||||
|
[1, shrink], strength='strong')
|
||||||
|
layoutbox.align([axpos, lbpos], 'h_center')
|
||||||
|
# set the height of the pos box
|
||||||
|
lbpos.constrain_height(axpos.width * aspect * shrink,
|
||||||
|
strength='medium')
|
||||||
|
|
||||||
|
return lb, lbpos
|
||||||
|
|
||||||
|
|
||||||
|
def _getmaxminrowcolumn(axs):
|
||||||
|
# helper to get the min/max rows and columns of a list of axes.
|
||||||
|
maxrow = -100000
|
||||||
|
minrow = 1000000
|
||||||
|
maxax = None
|
||||||
|
minax = None
|
||||||
|
maxcol = -100000
|
||||||
|
mincol = 1000000
|
||||||
|
maxax_col = None
|
||||||
|
minax_col = None
|
||||||
|
|
||||||
|
for ax in axs:
|
||||||
|
subspec = ax.get_subplotspec()
|
||||||
|
nrows, ncols, row_start, row_stop, col_start, col_stop = \
|
||||||
|
subspec.get_rows_columns()
|
||||||
|
if row_stop > maxrow:
|
||||||
|
maxrow = row_stop
|
||||||
|
maxax = ax
|
||||||
|
if row_start < minrow:
|
||||||
|
minrow = row_start
|
||||||
|
minax = ax
|
||||||
|
if col_stop > maxcol:
|
||||||
|
maxcol = col_stop
|
||||||
|
maxax_col = ax
|
||||||
|
if col_start < mincol:
|
||||||
|
mincol = col_start
|
||||||
|
minax_col = ax
|
||||||
|
return (minrow, maxrow, minax, maxax, mincol, maxcol, minax_col, maxax_col)
|
||||||
|
|
||||||
|
|
||||||
|
def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05):
|
||||||
|
"""
|
||||||
|
Do the layout for a colorbar, to not oeverly pollute colorbar.py
|
||||||
|
|
||||||
|
`pad` is in fraction of the original axis size.
|
||||||
|
"""
|
||||||
|
|
||||||
|
gs = parents[0].get_subplotspec().get_gridspec()
|
||||||
|
# parent layout box....
|
||||||
|
gslb = gs._layoutbox
|
||||||
|
|
||||||
|
lb = layoutbox.LayoutBox(parent=gslb.parent,
|
||||||
|
name=gslb.parent.name + '.cbar',
|
||||||
|
artist=cax)
|
||||||
|
# figure out the row and column extent of the parents.
|
||||||
|
(minrow, maxrow, minax_row, maxax_row,
|
||||||
|
mincol, maxcol, minax_col, maxax_col) = _getmaxminrowcolumn(parents)
|
||||||
|
|
||||||
|
if location in ('left', 'right'):
|
||||||
|
lbpos = layoutbox.LayoutBox(
|
||||||
|
parent=lb,
|
||||||
|
name=lb.name + '.pos',
|
||||||
|
tightwidth=False,
|
||||||
|
pos=True,
|
||||||
|
subplot=False,
|
||||||
|
artist=cax)
|
||||||
|
for ax in parents:
|
||||||
|
if location == 'right':
|
||||||
|
order = [ax._layoutbox, lb]
|
||||||
|
else:
|
||||||
|
order = [lb, ax._layoutbox]
|
||||||
|
layoutbox.hstack(order, padding=pad * gslb.width,
|
||||||
|
strength='strong')
|
||||||
|
# constrain the height and center...
|
||||||
|
# This isn't quite right. We'd like the colorbar
|
||||||
|
# pos to line up w/ the axes poss, not the size of the
|
||||||
|
# gs.
|
||||||
|
|
||||||
|
# Horizontal Layout: need to check all the axes in this gridspec
|
||||||
|
for ch in gslb.children:
|
||||||
|
subspec = ch.artist
|
||||||
|
nrows, ncols, row_start, row_stop, col_start, col_stop = \
|
||||||
|
subspec.get_rows_columns()
|
||||||
|
if location == 'right':
|
||||||
|
if col_stop <= maxcol:
|
||||||
|
order = [subspec._layoutbox, lb]
|
||||||
|
# arrange to right of the parents
|
||||||
|
if col_start > maxcol:
|
||||||
|
order = [lb, subspec._layoutbox]
|
||||||
|
elif location == 'left':
|
||||||
|
if col_start >= mincol:
|
||||||
|
order = [lb, subspec._layoutbox]
|
||||||
|
if col_stop < mincol:
|
||||||
|
order = [subspec._layoutbox, lb]
|
||||||
|
layoutbox.hstack(order, padding=pad * gslb.width,
|
||||||
|
strength='strong')
|
||||||
|
|
||||||
|
# Vertical layout:
|
||||||
|
maxposlb = minax_row._poslayoutbox
|
||||||
|
minposlb = maxax_row._poslayoutbox
|
||||||
|
# now we want the height of the colorbar pos to be
|
||||||
|
# set by the top and bottom of the min/max axes...
|
||||||
|
# bottom top
|
||||||
|
# b t
|
||||||
|
# h = (top-bottom)*shrink
|
||||||
|
# b = bottom + (top-bottom - h) / 2.
|
||||||
|
lbpos.constrain_height(
|
||||||
|
(maxposlb.top - minposlb.bottom) *
|
||||||
|
shrink, strength='strong')
|
||||||
|
lbpos.constrain_bottom(
|
||||||
|
(maxposlb.top - minposlb.bottom) *
|
||||||
|
(1 - shrink)/2 + minposlb.bottom,
|
||||||
|
strength='strong')
|
||||||
|
|
||||||
|
# set the width of the pos box
|
||||||
|
lbpos.constrain_width(lbpos.height * (shrink / aspect),
|
||||||
|
strength='strong')
|
||||||
|
elif location in ('bottom', 'top'):
|
||||||
|
lbpos = layoutbox.LayoutBox(
|
||||||
|
parent=lb,
|
||||||
|
name=lb.name + '.pos',
|
||||||
|
tightheight=True,
|
||||||
|
pos=True,
|
||||||
|
subplot=False,
|
||||||
|
artist=cax)
|
||||||
|
|
||||||
|
for ax in parents:
|
||||||
|
if location == 'bottom':
|
||||||
|
order = [ax._layoutbox, lb]
|
||||||
|
else:
|
||||||
|
order = [lb, ax._layoutbox]
|
||||||
|
layoutbox.vstack(order, padding=pad * gslb.width,
|
||||||
|
strength='strong')
|
||||||
|
|
||||||
|
# Vertical Layout: need to check all the axes in this gridspec
|
||||||
|
for ch in gslb.children:
|
||||||
|
subspec = ch.artist
|
||||||
|
nrows, ncols, row_start, row_stop, col_start, col_stop = \
|
||||||
|
subspec.get_rows_columns()
|
||||||
|
if location == 'bottom':
|
||||||
|
if row_stop <= minrow:
|
||||||
|
order = [subspec._layoutbox, lb]
|
||||||
|
if row_start > maxrow:
|
||||||
|
order = [lb, subspec._layoutbox]
|
||||||
|
elif location == 'top':
|
||||||
|
if row_stop < minrow:
|
||||||
|
order = [subspec._layoutbox, lb]
|
||||||
|
if row_start >= maxrow:
|
||||||
|
order = [lb, subspec._layoutbox]
|
||||||
|
layoutbox.vstack(order, padding=pad * gslb.width,
|
||||||
|
strength='strong')
|
||||||
|
|
||||||
|
# Do horizontal layout...
|
||||||
|
maxposlb = maxax_col._poslayoutbox
|
||||||
|
minposlb = minax_col._poslayoutbox
|
||||||
|
lbpos.constrain_width((maxposlb.right - minposlb.left) *
|
||||||
|
shrink)
|
||||||
|
lbpos.constrain_left(
|
||||||
|
(maxposlb.right - minposlb.left) *
|
||||||
|
(1-shrink)/2 + minposlb.left)
|
||||||
|
# set the height of the pos box
|
||||||
|
lbpos.constrain_height(lbpos.width * shrink * aspect,
|
||||||
|
strength='medium')
|
||||||
|
|
||||||
|
return lb, lbpos
|
||||||
@@ -0,0 +1,735 @@
|
|||||||
|
"""
|
||||||
|
|
||||||
|
Conventions:
|
||||||
|
|
||||||
|
"constrain_x" means to constrain the variable with either
|
||||||
|
another kiwisolver variable, or a float. i.e. `constrain_width(0.2)`
|
||||||
|
will set a constraint that the width has to be 0.2 and this constraint is
|
||||||
|
permanent - i.e. it will not be removed if it becomes obsolete.
|
||||||
|
|
||||||
|
"edit_x" means to set x to a value (just a float), and that this value can
|
||||||
|
change. So `edit_width(0.2)` will set width to be 0.2, but `edit_width(0.3)`
|
||||||
|
will allow it to change to 0.3 later. Note that these values are still just
|
||||||
|
"suggestions" in `kiwisolver` parlance, and could be over-ridden by
|
||||||
|
other constrains.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import itertools
|
||||||
|
import kiwisolver as kiwi
|
||||||
|
import logging
|
||||||
|
import numpy as np
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# renderers can be complicated
|
||||||
|
def get_renderer(fig):
|
||||||
|
if fig._cachedRenderer:
|
||||||
|
renderer = fig._cachedRenderer
|
||||||
|
else:
|
||||||
|
canvas = fig.canvas
|
||||||
|
if canvas and hasattr(canvas, "get_renderer"):
|
||||||
|
renderer = canvas.get_renderer()
|
||||||
|
else:
|
||||||
|
# not sure if this can happen
|
||||||
|
# seems to with PDF...
|
||||||
|
_log.info("constrained_layout : falling back to Agg renderer")
|
||||||
|
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
||||||
|
canvas = FigureCanvasAgg(fig)
|
||||||
|
renderer = canvas.get_renderer()
|
||||||
|
|
||||||
|
return renderer
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutBox(object):
|
||||||
|
"""
|
||||||
|
Basic rectangle representation using kiwi solver variables
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None, name='', tightwidth=False,
|
||||||
|
tightheight=False, artist=None,
|
||||||
|
lower_left=(0, 0), upper_right=(1, 1), pos=False,
|
||||||
|
subplot=False, h_pad=None, w_pad=None):
|
||||||
|
Variable = kiwi.Variable
|
||||||
|
self.parent = parent
|
||||||
|
self.name = name
|
||||||
|
sn = self.name + '_'
|
||||||
|
if parent is None:
|
||||||
|
self.solver = kiwi.Solver()
|
||||||
|
self.constrained_layout_called = 0
|
||||||
|
else:
|
||||||
|
self.solver = parent.solver
|
||||||
|
self.constrained_layout_called = None
|
||||||
|
# parent wants to know about this child!
|
||||||
|
parent.add_child(self)
|
||||||
|
# keep track of artist associated w/ this layout. Can be none
|
||||||
|
self.artist = artist
|
||||||
|
# keep track if this box is supposed to be a pos that is constrained
|
||||||
|
# by the parent.
|
||||||
|
self.pos = pos
|
||||||
|
# keep track of whether we need to match this subplot up with others.
|
||||||
|
self.subplot = subplot
|
||||||
|
|
||||||
|
# we need the str below for Py 2 which complains the string is unicode
|
||||||
|
self.top = Variable(str(sn + 'top'))
|
||||||
|
self.bottom = Variable(str(sn + 'bottom'))
|
||||||
|
self.left = Variable(str(sn + 'left'))
|
||||||
|
self.right = Variable(str(sn + 'right'))
|
||||||
|
|
||||||
|
self.width = Variable(str(sn + 'width'))
|
||||||
|
self.height = Variable(str(sn + 'height'))
|
||||||
|
self.h_center = Variable(str(sn + 'h_center'))
|
||||||
|
self.v_center = Variable(str(sn + 'v_center'))
|
||||||
|
|
||||||
|
self.min_width = Variable(str(sn + 'min_width'))
|
||||||
|
self.min_height = Variable(str(sn + 'min_height'))
|
||||||
|
self.pref_width = Variable(str(sn + 'pref_width'))
|
||||||
|
self.pref_height = Variable(str(sn + 'pref_height'))
|
||||||
|
# margis are only used for axes-position layout boxes. maybe should
|
||||||
|
# be a separate subclass:
|
||||||
|
self.left_margin = Variable(str(sn + 'left_margin'))
|
||||||
|
self.right_margin = Variable(str(sn + 'right_margin'))
|
||||||
|
self.bottom_margin = Variable(str(sn + 'bottom_margin'))
|
||||||
|
self.top_margin = Variable(str(sn + 'top_margin'))
|
||||||
|
# mins
|
||||||
|
self.left_margin_min = Variable(str(sn + 'left_margin_min'))
|
||||||
|
self.right_margin_min = Variable(str(sn + 'right_margin_min'))
|
||||||
|
self.bottom_margin_min = Variable(str(sn + 'bottom_margin_min'))
|
||||||
|
self.top_margin_min = Variable(str(sn + 'top_margin_min'))
|
||||||
|
|
||||||
|
right, top = upper_right
|
||||||
|
left, bottom = lower_left
|
||||||
|
self.tightheight = tightheight
|
||||||
|
self.tightwidth = tightwidth
|
||||||
|
self.add_constraints()
|
||||||
|
self.children = []
|
||||||
|
self.subplotspec = None
|
||||||
|
if self.pos:
|
||||||
|
self.constrain_margins()
|
||||||
|
self.h_pad = h_pad
|
||||||
|
self.w_pad = w_pad
|
||||||
|
|
||||||
|
def constrain_margins(self):
|
||||||
|
"""
|
||||||
|
Only do this for pos. This sets a variable distance
|
||||||
|
margin between the position of the axes and the outer edge of
|
||||||
|
the axes.
|
||||||
|
|
||||||
|
Margins are variable because they change with the fogure size.
|
||||||
|
|
||||||
|
Margin minimums are set to make room for axes decorations. However,
|
||||||
|
the margins can be larger if we are mathicng the position size to
|
||||||
|
otehr axes.
|
||||||
|
"""
|
||||||
|
sol = self.solver
|
||||||
|
|
||||||
|
# left
|
||||||
|
if not sol.hasEditVariable(self.left_margin_min):
|
||||||
|
sol.addEditVariable(self.left_margin_min, 'strong')
|
||||||
|
sol.suggestValue(self.left_margin_min, 0.0001)
|
||||||
|
c = (self.left_margin == self.left - self.parent.left)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
c = (self.left_margin >= self.left_margin_min)
|
||||||
|
self.solver.addConstraint(c | 'strong')
|
||||||
|
|
||||||
|
# right
|
||||||
|
if not sol.hasEditVariable(self.right_margin_min):
|
||||||
|
sol.addEditVariable(self.right_margin_min, 'strong')
|
||||||
|
sol.suggestValue(self.right_margin_min, 0.0001)
|
||||||
|
c = (self.right_margin == self.parent.right - self.right)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
c = (self.right_margin >= self.right_margin_min)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
# bottom
|
||||||
|
if not sol.hasEditVariable(self.bottom_margin_min):
|
||||||
|
sol.addEditVariable(self.bottom_margin_min, 'strong')
|
||||||
|
sol.suggestValue(self.bottom_margin_min, 0.0001)
|
||||||
|
c = (self.bottom_margin == self.bottom - self.parent.bottom)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
c = (self.bottom_margin >= self.bottom_margin_min)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
# top
|
||||||
|
if not sol.hasEditVariable(self.top_margin_min):
|
||||||
|
sol.addEditVariable(self.top_margin_min, 'strong')
|
||||||
|
sol.suggestValue(self.top_margin_min, 0.0001)
|
||||||
|
c = (self.top_margin == self.parent.top - self.top)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
c = (self.top_margin >= self.top_margin_min)
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
|
||||||
|
def add_child(self, child):
|
||||||
|
self.children += [child]
|
||||||
|
|
||||||
|
def remove_child(self, child):
|
||||||
|
try:
|
||||||
|
self.children.remove(child)
|
||||||
|
except ValueError:
|
||||||
|
_log.info("Tried to remove child that doesn't belong to parent")
|
||||||
|
|
||||||
|
def add_constraints(self):
|
||||||
|
sol = self.solver
|
||||||
|
# never let width and height go negative.
|
||||||
|
for i in [self.min_width, self.min_height]:
|
||||||
|
sol.addEditVariable(i, 1e9)
|
||||||
|
sol.suggestValue(i, 0.0)
|
||||||
|
# define relation ships between things thing width and right and left
|
||||||
|
self.hard_constraints()
|
||||||
|
# self.soft_constraints()
|
||||||
|
if self.parent:
|
||||||
|
self.parent_constrain()
|
||||||
|
# sol.updateVariables()
|
||||||
|
|
||||||
|
def parent_constrain(self):
|
||||||
|
parent = self.parent
|
||||||
|
hc = [self.left >= parent.left,
|
||||||
|
self.bottom >= parent.bottom,
|
||||||
|
self.top <= parent.top,
|
||||||
|
self.right <= parent.right]
|
||||||
|
for c in hc:
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
|
||||||
|
def hard_constraints(self):
|
||||||
|
hc = [self.width == self.right - self.left,
|
||||||
|
self.height == self.top - self.bottom,
|
||||||
|
self.h_center == (self.left + self.right) * 0.5,
|
||||||
|
self.v_center == (self.top + self.bottom) * 0.5,
|
||||||
|
self.width >= self.min_width,
|
||||||
|
self.height >= self.min_height]
|
||||||
|
for c in hc:
|
||||||
|
self.solver.addConstraint(c | 'required')
|
||||||
|
|
||||||
|
def soft_constraints(self):
|
||||||
|
sol = self.solver
|
||||||
|
if self.tightwidth:
|
||||||
|
suggest = 0.
|
||||||
|
else:
|
||||||
|
suggest = 20.
|
||||||
|
c = (self.pref_width == suggest)
|
||||||
|
for i in c:
|
||||||
|
sol.addConstraint(i | 'required')
|
||||||
|
if self.tightheight:
|
||||||
|
suggest = 0.
|
||||||
|
else:
|
||||||
|
suggest = 20.
|
||||||
|
c = (self.pref_height == suggest)
|
||||||
|
for i in c:
|
||||||
|
sol.addConstraint(i | 'required')
|
||||||
|
|
||||||
|
c = [(self.width >= suggest),
|
||||||
|
(self.height >= suggest)]
|
||||||
|
for i in c:
|
||||||
|
sol.addConstraint(i | 150000)
|
||||||
|
|
||||||
|
def set_parent(self, parent):
|
||||||
|
''' replace the parent of this with the new parent
|
||||||
|
'''
|
||||||
|
self.parent = parent
|
||||||
|
self.parent_constrain()
|
||||||
|
|
||||||
|
def constrain_geometry(self, left, bottom, right, top, strength='strong'):
|
||||||
|
hc = [self.left == left,
|
||||||
|
self.right == right,
|
||||||
|
self.bottom == bottom,
|
||||||
|
self.top == top]
|
||||||
|
for c in hc:
|
||||||
|
self.solver.addConstraint((c | strength))
|
||||||
|
# self.solver.updateVariables()
|
||||||
|
|
||||||
|
def constrain_same(self, other, strength='strong'):
|
||||||
|
"""
|
||||||
|
Make the layoutbox have same position as other layoutbox
|
||||||
|
"""
|
||||||
|
hc = [self.left == other.left,
|
||||||
|
self.right == other.right,
|
||||||
|
self.bottom == other.bottom,
|
||||||
|
self.top == other.top]
|
||||||
|
for c in hc:
|
||||||
|
self.solver.addConstraint((c | strength))
|
||||||
|
|
||||||
|
def constrain_left_margin(self, margin, strength='strong'):
|
||||||
|
c = (self.left == self.parent.left + margin)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def edit_left_margin_min(self, margin):
|
||||||
|
self.solver.suggestValue(self.left_margin_min, margin)
|
||||||
|
|
||||||
|
def constrain_right_margin(self, margin, strength='strong'):
|
||||||
|
c = (self.right == self.parent.right - margin)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def edit_right_margin_min(self, margin):
|
||||||
|
self.solver.suggestValue(self.right_margin_min, margin)
|
||||||
|
|
||||||
|
def constrain_bottom_margin(self, margin, strength='strong'):
|
||||||
|
c = (self.bottom == self.parent.bottom + margin)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def edit_bottom_margin_min(self, margin):
|
||||||
|
self.solver.suggestValue(self.bottom_margin_min, margin)
|
||||||
|
|
||||||
|
def constrain_top_margin(self, margin, strength='strong'):
|
||||||
|
c = (self.top == self.parent.top - margin)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def edit_top_margin_min(self, margin):
|
||||||
|
self.solver.suggestValue(self.top_margin_min, margin)
|
||||||
|
|
||||||
|
def get_rect(self):
|
||||||
|
return (self.left.value(), self.bottom.value(),
|
||||||
|
self.width.value(), self.height.value())
|
||||||
|
|
||||||
|
def update_variables(self):
|
||||||
|
'''
|
||||||
|
Update *all* the variables that are part of the solver this LayoutBox
|
||||||
|
is created with
|
||||||
|
'''
|
||||||
|
self.solver.updateVariables()
|
||||||
|
|
||||||
|
def edit_height(self, height, strength='strong'):
|
||||||
|
'''
|
||||||
|
Set the height of the layout box.
|
||||||
|
|
||||||
|
This is done as an editable variable so that the value can change
|
||||||
|
due to resizing.
|
||||||
|
'''
|
||||||
|
sol = self.solver
|
||||||
|
for i in [self.height]:
|
||||||
|
if not sol.hasEditVariable(i):
|
||||||
|
sol.addEditVariable(i, strength)
|
||||||
|
sol.suggestValue(self.height, height)
|
||||||
|
|
||||||
|
def constrain_height(self, height, strength='strong'):
|
||||||
|
'''
|
||||||
|
Constrain the height of the layout box. height is
|
||||||
|
either a float or a layoutbox.height.
|
||||||
|
'''
|
||||||
|
c = (self.height == height)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def constrain_height_min(self, height, strength='strong'):
|
||||||
|
c = (self.height >= height)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def edit_width(self, width, strength='strong'):
|
||||||
|
sol = self.solver
|
||||||
|
for i in [self.width]:
|
||||||
|
if not sol.hasEditVariable(i):
|
||||||
|
sol.addEditVariable(i, strength)
|
||||||
|
sol.suggestValue(self.width, width)
|
||||||
|
|
||||||
|
def constrain_width(self, width, strength='strong'):
|
||||||
|
'''
|
||||||
|
Constrain the width of the layout box. `width` is
|
||||||
|
either a float or a layoutbox.width.
|
||||||
|
'''
|
||||||
|
c = (self.width == width)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def constrain_width_min(self, width, strength='strong'):
|
||||||
|
c = (self.width >= width)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def constrain_left(self, left, strength='strong'):
|
||||||
|
c = (self.left == left)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def constrain_bottom(self, bottom, strength='strong'):
|
||||||
|
c = (self.bottom == bottom)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def constrain_right(self, right, strength='strong'):
|
||||||
|
c = (self.right == right)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def constrain_top(self, top, strength='strong'):
|
||||||
|
c = (self.top == top)
|
||||||
|
self.solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
def _is_subplotspec_layoutbox(self):
|
||||||
|
'''
|
||||||
|
Helper to check if this layoutbox is the layoutbox of a
|
||||||
|
subplotspec
|
||||||
|
'''
|
||||||
|
name = (self.name).split('.')[-1]
|
||||||
|
return name[:2] == 'ss'
|
||||||
|
|
||||||
|
def _is_gridspec_layoutbox(self):
|
||||||
|
'''
|
||||||
|
Helper to check if this layoutbox is the layoutbox of a
|
||||||
|
gridspec
|
||||||
|
'''
|
||||||
|
name = (self.name).split('.')[-1]
|
||||||
|
return name[:8] == 'gridspec'
|
||||||
|
|
||||||
|
def find_child_subplots(self):
|
||||||
|
'''
|
||||||
|
Find children of this layout box that are subplots. We want to line
|
||||||
|
poss up, and this is an easy way to find them all.
|
||||||
|
'''
|
||||||
|
if self.subplot:
|
||||||
|
subplots = [self]
|
||||||
|
else:
|
||||||
|
subplots = []
|
||||||
|
for child in self.children:
|
||||||
|
subplots += child.find_child_subplots()
|
||||||
|
return subplots
|
||||||
|
|
||||||
|
def layout_from_subplotspec(self, subspec,
|
||||||
|
name='', artist=None, pos=False):
|
||||||
|
''' Make a layout box from a subplotspec. The layout box is
|
||||||
|
constrained to be a fraction of the width/height of the parent,
|
||||||
|
and be a fraction of the parent width/height from the left/bottom
|
||||||
|
of the parent. Therefore the parent can move around and the
|
||||||
|
layout for the subplot spec should move with it.
|
||||||
|
|
||||||
|
The parent is *usually* the gridspec that made the subplotspec.??
|
||||||
|
'''
|
||||||
|
lb = LayoutBox(parent=self, name=name, artist=artist, pos=pos)
|
||||||
|
gs = subspec.get_gridspec()
|
||||||
|
nrows, ncols = gs.get_geometry()
|
||||||
|
parent = self.parent
|
||||||
|
|
||||||
|
# OK, now, we want to set the position of this subplotspec
|
||||||
|
# based on its subplotspec parameters. The new gridspec will inherit.
|
||||||
|
|
||||||
|
# from gridspec. prob should be new method in gridspec
|
||||||
|
left = 0.0
|
||||||
|
right = 1.0
|
||||||
|
bottom = 0.0
|
||||||
|
top = 1.0
|
||||||
|
totWidth = right-left
|
||||||
|
totHeight = top-bottom
|
||||||
|
hspace = 0.
|
||||||
|
wspace = 0.
|
||||||
|
|
||||||
|
# calculate accumulated heights of columns
|
||||||
|
cellH = totHeight / (nrows + hspace * (nrows - 1))
|
||||||
|
sepH = hspace*cellH
|
||||||
|
|
||||||
|
if gs._row_height_ratios is not None:
|
||||||
|
netHeight = cellH * nrows
|
||||||
|
tr = float(sum(gs._row_height_ratios))
|
||||||
|
cellHeights = [netHeight*r/tr for r in gs._row_height_ratios]
|
||||||
|
else:
|
||||||
|
cellHeights = [cellH] * nrows
|
||||||
|
|
||||||
|
sepHeights = [0] + ([sepH] * (nrows - 1))
|
||||||
|
cellHs = np.add.accumulate(np.ravel(
|
||||||
|
list(zip(sepHeights, cellHeights))))
|
||||||
|
|
||||||
|
# calculate accumulated widths of rows
|
||||||
|
cellW = totWidth/(ncols + wspace * (ncols - 1))
|
||||||
|
sepW = wspace*cellW
|
||||||
|
|
||||||
|
if gs._col_width_ratios is not None:
|
||||||
|
netWidth = cellW * ncols
|
||||||
|
tr = float(sum(gs._col_width_ratios))
|
||||||
|
cellWidths = [netWidth * r / tr for r in gs._col_width_ratios]
|
||||||
|
else:
|
||||||
|
cellWidths = [cellW] * ncols
|
||||||
|
|
||||||
|
sepWidths = [0] + ([sepW] * (ncols - 1))
|
||||||
|
cellWs = np.add.accumulate(np.ravel(list(zip(sepWidths, cellWidths))))
|
||||||
|
|
||||||
|
figTops = [top - cellHs[2 * rowNum] for rowNum in range(nrows)]
|
||||||
|
figBottoms = [top - cellHs[2 * rowNum + 1] for rowNum in range(nrows)]
|
||||||
|
figLefts = [left + cellWs[2 * colNum] for colNum in range(ncols)]
|
||||||
|
figRights = [left + cellWs[2 * colNum + 1] for colNum in range(ncols)]
|
||||||
|
|
||||||
|
rowNum, colNum = divmod(subspec.num1, ncols)
|
||||||
|
figBottom = figBottoms[rowNum]
|
||||||
|
figTop = figTops[rowNum]
|
||||||
|
figLeft = figLefts[colNum]
|
||||||
|
figRight = figRights[colNum]
|
||||||
|
|
||||||
|
if subspec.num2 is not None:
|
||||||
|
|
||||||
|
rowNum2, colNum2 = divmod(subspec.num2, ncols)
|
||||||
|
figBottom2 = figBottoms[rowNum2]
|
||||||
|
figTop2 = figTops[rowNum2]
|
||||||
|
figLeft2 = figLefts[colNum2]
|
||||||
|
figRight2 = figRights[colNum2]
|
||||||
|
|
||||||
|
figBottom = min(figBottom, figBottom2)
|
||||||
|
figLeft = min(figLeft, figLeft2)
|
||||||
|
figTop = max(figTop, figTop2)
|
||||||
|
figRight = max(figRight, figRight2)
|
||||||
|
# These are numbers relative to 0,0,1,1. Need to constrain
|
||||||
|
# relative to parent.
|
||||||
|
|
||||||
|
width = figRight - figLeft
|
||||||
|
height = figTop - figBottom
|
||||||
|
parent = self.parent
|
||||||
|
cs = [self.left == parent.left + parent.width * figLeft,
|
||||||
|
self.bottom == parent.bottom + parent.height * figBottom,
|
||||||
|
self.width == parent.width * width,
|
||||||
|
self.height == parent.height * height]
|
||||||
|
for c in cs:
|
||||||
|
self.solver.addConstraint((c | 'required'))
|
||||||
|
|
||||||
|
return lb
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
args = (self.name, self.left.value(), self.bottom.value(),
|
||||||
|
self.right.value(), self.top.value())
|
||||||
|
return ('LayoutBox: %25s, (left: %1.3f) (bot: %1.3f) '
|
||||||
|
'(right: %1.3f) (top: %1.3f) ') % args
|
||||||
|
|
||||||
|
|
||||||
|
# Utility functions that act on layoutboxes...
|
||||||
|
def hstack(boxes, padding=0, strength='strong'):
|
||||||
|
'''
|
||||||
|
Stack LayoutBox instances from left to right.
|
||||||
|
`padding` is in figure-relative units.
|
||||||
|
'''
|
||||||
|
|
||||||
|
for i in range(1, len(boxes)):
|
||||||
|
c = (boxes[i-1].right + padding <= boxes[i].left)
|
||||||
|
boxes[i].solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def hpack(boxes, padding=0, strength='strong'):
|
||||||
|
'''
|
||||||
|
Stack LayoutBox instances from left to right.
|
||||||
|
'''
|
||||||
|
|
||||||
|
for i in range(1, len(boxes)):
|
||||||
|
c = (boxes[i-1].right + padding == boxes[i].left)
|
||||||
|
boxes[i].solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def vstack(boxes, padding=0, strength='strong'):
|
||||||
|
'''
|
||||||
|
Stack LayoutBox instances from top to bottom
|
||||||
|
'''
|
||||||
|
|
||||||
|
for i in range(1, len(boxes)):
|
||||||
|
c = (boxes[i-1].bottom - padding >= boxes[i].top)
|
||||||
|
boxes[i].solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def vpack(boxes, padding=0, strength='strong'):
|
||||||
|
'''
|
||||||
|
Stack LayoutBox instances from top to bottom
|
||||||
|
'''
|
||||||
|
|
||||||
|
for i in range(1, len(boxes)):
|
||||||
|
c = (boxes[i-1].bottom - padding >= boxes[i].top)
|
||||||
|
boxes[i].solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def match_heights(boxes, height_ratios=None, strength='medium'):
|
||||||
|
'''
|
||||||
|
Stack LayoutBox instances from top to bottom
|
||||||
|
'''
|
||||||
|
|
||||||
|
if height_ratios is None:
|
||||||
|
height_ratios = np.ones(len(boxes))
|
||||||
|
for i in range(1, len(boxes)):
|
||||||
|
c = (boxes[i-1].height ==
|
||||||
|
boxes[i].height*height_ratios[i-1]/height_ratios[i])
|
||||||
|
boxes[i].solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def match_widths(boxes, width_ratios=None, strength='medium'):
|
||||||
|
'''
|
||||||
|
Stack LayoutBox instances from top to bottom
|
||||||
|
'''
|
||||||
|
|
||||||
|
if width_ratios is None:
|
||||||
|
width_ratios = np.ones(len(boxes))
|
||||||
|
for i in range(1, len(boxes)):
|
||||||
|
c = (boxes[i-1].width ==
|
||||||
|
boxes[i].width*width_ratios[i-1]/width_ratios[i])
|
||||||
|
boxes[i].solver.addConstraint(c | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def vstackeq(boxes, padding=0, height_ratios=None):
|
||||||
|
vstack(boxes, padding=padding)
|
||||||
|
match_heights(boxes, height_ratios=height_ratios)
|
||||||
|
|
||||||
|
|
||||||
|
def hstackeq(boxes, padding=0, width_ratios=None):
|
||||||
|
hstack(boxes, padding=padding)
|
||||||
|
match_widths(boxes, width_ratios=width_ratios)
|
||||||
|
|
||||||
|
|
||||||
|
def align(boxes, attr, strength='strong'):
|
||||||
|
cons = []
|
||||||
|
for box in boxes[1:]:
|
||||||
|
cons = (getattr(boxes[0], attr) == getattr(box, attr))
|
||||||
|
boxes[0].solver.addConstraint(cons | strength)
|
||||||
|
|
||||||
|
|
||||||
|
def match_top_margins(boxes, levels=1):
|
||||||
|
box0 = boxes[0]
|
||||||
|
top0 = box0
|
||||||
|
for n in range(levels):
|
||||||
|
top0 = top0.parent
|
||||||
|
for box in boxes[1:]:
|
||||||
|
topb = box
|
||||||
|
for n in range(levels):
|
||||||
|
topb = topb.parent
|
||||||
|
c = (box0.top-top0.top == box.top-topb.top)
|
||||||
|
box0.solver.addConstraint(c | 'strong')
|
||||||
|
|
||||||
|
|
||||||
|
def match_bottom_margins(boxes, levels=1):
|
||||||
|
box0 = boxes[0]
|
||||||
|
top0 = box0
|
||||||
|
for n in range(levels):
|
||||||
|
top0 = top0.parent
|
||||||
|
for box in boxes[1:]:
|
||||||
|
topb = box
|
||||||
|
for n in range(levels):
|
||||||
|
topb = topb.parent
|
||||||
|
c = (box0.bottom-top0.bottom == box.bottom-topb.bottom)
|
||||||
|
box0.solver.addConstraint(c | 'strong')
|
||||||
|
|
||||||
|
|
||||||
|
def match_left_margins(boxes, levels=1):
|
||||||
|
box0 = boxes[0]
|
||||||
|
top0 = box0
|
||||||
|
for n in range(levels):
|
||||||
|
top0 = top0.parent
|
||||||
|
for box in boxes[1:]:
|
||||||
|
topb = box
|
||||||
|
for n in range(levels):
|
||||||
|
topb = topb.parent
|
||||||
|
c = (box0.left-top0.left == box.left-topb.left)
|
||||||
|
box0.solver.addConstraint(c | 'strong')
|
||||||
|
|
||||||
|
|
||||||
|
def match_right_margins(boxes, levels=1):
|
||||||
|
box0 = boxes[0]
|
||||||
|
top0 = box0
|
||||||
|
for n in range(levels):
|
||||||
|
top0 = top0.parent
|
||||||
|
for box in boxes[1:]:
|
||||||
|
topb = box
|
||||||
|
for n in range(levels):
|
||||||
|
topb = topb.parent
|
||||||
|
c = (box0.right-top0.right == box.right-topb.right)
|
||||||
|
box0.solver.addConstraint(c | 'strong')
|
||||||
|
|
||||||
|
|
||||||
|
def match_width_margins(boxes, levels=1):
|
||||||
|
match_left_margins(boxes, levels=levels)
|
||||||
|
match_right_margins(boxes, levels=levels)
|
||||||
|
|
||||||
|
|
||||||
|
def match_height_margins(boxes, levels=1):
|
||||||
|
match_top_margins(boxes, levels=levels)
|
||||||
|
match_bottom_margins(boxes, levels=levels)
|
||||||
|
|
||||||
|
|
||||||
|
def match_margins(boxes, levels=1):
|
||||||
|
match_width_margins(boxes, levels=levels)
|
||||||
|
match_height_margins(boxes, levels=levels)
|
||||||
|
|
||||||
|
|
||||||
|
_layoutboxobjnum = itertools.count()
|
||||||
|
|
||||||
|
|
||||||
|
def seq_id():
|
||||||
|
'''
|
||||||
|
Generate a short sequential id for layoutbox objects...
|
||||||
|
'''
|
||||||
|
|
||||||
|
global _layoutboxobjnum
|
||||||
|
|
||||||
|
return ('%06d' % (next(_layoutboxobjnum)))
|
||||||
|
|
||||||
|
|
||||||
|
def print_children(lb):
|
||||||
|
'''
|
||||||
|
Print the children of the layoutbox
|
||||||
|
'''
|
||||||
|
print(lb)
|
||||||
|
for child in lb.children:
|
||||||
|
print_children(child)
|
||||||
|
|
||||||
|
|
||||||
|
def nonetree(lb):
|
||||||
|
'''
|
||||||
|
Make all elements in this tree none... This signals not to do any more
|
||||||
|
layout.
|
||||||
|
'''
|
||||||
|
if lb is not None:
|
||||||
|
if lb.parent is None:
|
||||||
|
# Clear the solver. Hopefully this garbage collects.
|
||||||
|
lb.solver.reset()
|
||||||
|
nonechildren(lb)
|
||||||
|
else:
|
||||||
|
nonetree(lb.parent)
|
||||||
|
|
||||||
|
|
||||||
|
def nonechildren(lb):
|
||||||
|
for child in lb.children:
|
||||||
|
nonechildren(child)
|
||||||
|
lb.artist._layoutbox = None
|
||||||
|
lb = None
|
||||||
|
|
||||||
|
|
||||||
|
def print_tree(lb):
|
||||||
|
'''
|
||||||
|
Print the tree of layoutboxes
|
||||||
|
'''
|
||||||
|
|
||||||
|
if lb.parent is None:
|
||||||
|
print('LayoutBox Tree\n')
|
||||||
|
print('==============\n')
|
||||||
|
print_children(lb)
|
||||||
|
print('\n')
|
||||||
|
else:
|
||||||
|
print_tree(lb.parent)
|
||||||
|
|
||||||
|
|
||||||
|
def plot_children(fig, box, level=0, printit=True):
|
||||||
|
'''
|
||||||
|
Simple plotting to show where boxes are
|
||||||
|
'''
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
if isinstance(fig, matplotlib.figure.Figure):
|
||||||
|
ax = fig.add_axes([0., 0., 1., 1.])
|
||||||
|
ax.set_facecolor([1., 1., 1., 0.7])
|
||||||
|
ax.set_alpha(0.3)
|
||||||
|
fig.draw(fig.canvas.get_renderer())
|
||||||
|
else:
|
||||||
|
ax = fig
|
||||||
|
|
||||||
|
import matplotlib.patches as patches
|
||||||
|
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
||||||
|
if printit:
|
||||||
|
print("Level:", level)
|
||||||
|
for child in box.children:
|
||||||
|
rect = child.get_rect()
|
||||||
|
if printit:
|
||||||
|
print(child)
|
||||||
|
ax.add_patch(
|
||||||
|
patches.Rectangle(
|
||||||
|
(child.left.value(), child.bottom.value()), # (x,y)
|
||||||
|
child.width.value(), # width
|
||||||
|
child.height.value(), # height
|
||||||
|
fc='none',
|
||||||
|
alpha=0.8,
|
||||||
|
ec=colors[level]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if level > 0:
|
||||||
|
name = child.name.split('.')[-1]
|
||||||
|
if level % 2 == 0:
|
||||||
|
ax.text(child.left.value(), child.bottom.value(), name,
|
||||||
|
size=12-level, color=colors[level])
|
||||||
|
else:
|
||||||
|
ax.text(child.right.value(), child.top.value(), name,
|
||||||
|
ha='right', va='top', size=12-level,
|
||||||
|
color=colors[level])
|
||||||
|
|
||||||
|
plot_children(ax, child, level=level+1, printit=printit)
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
Manage figures for pyplot interface.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import atexit
|
||||||
|
import gc
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class Gcf(object):
|
||||||
|
"""
|
||||||
|
Singleton to manage a set of integer-numbered figures.
|
||||||
|
|
||||||
|
This class is never instantiated; it consists of two class
|
||||||
|
attributes (a list and a dictionary), and a set of static
|
||||||
|
methods that operate on those attributes, accessing them
|
||||||
|
directly as class attributes.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
|
||||||
|
*figs*:
|
||||||
|
dictionary of the form {*num*: *manager*, ...}
|
||||||
|
|
||||||
|
*_activeQue*:
|
||||||
|
list of *managers*, with active one at the end
|
||||||
|
|
||||||
|
"""
|
||||||
|
_activeQue = []
|
||||||
|
figs = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fig_manager(cls, num):
|
||||||
|
"""
|
||||||
|
If figure manager *num* exists, make it the active
|
||||||
|
figure and return the manager; otherwise return *None*.
|
||||||
|
"""
|
||||||
|
manager = cls.figs.get(num, None)
|
||||||
|
if manager is not None:
|
||||||
|
cls.set_active(manager)
|
||||||
|
return manager
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy(cls, num):
|
||||||
|
"""
|
||||||
|
Try to remove all traces of figure *num*.
|
||||||
|
|
||||||
|
In the interactive backends, this is bound to the
|
||||||
|
window "destroy" and "delete" events.
|
||||||
|
"""
|
||||||
|
if not cls.has_fignum(num):
|
||||||
|
return
|
||||||
|
manager = cls.figs[num]
|
||||||
|
manager.canvas.mpl_disconnect(manager._cidgcf)
|
||||||
|
cls._activeQue.remove(manager)
|
||||||
|
del cls.figs[num]
|
||||||
|
manager.destroy()
|
||||||
|
gc.collect(1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy_fig(cls, fig):
|
||||||
|
"*fig* is a Figure instance"
|
||||||
|
num = next((manager.num for manager in cls.figs.values()
|
||||||
|
if manager.canvas.figure == fig), None)
|
||||||
|
if num is not None:
|
||||||
|
cls.destroy(num)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def destroy_all(cls):
|
||||||
|
# this is need to ensure that gc is available in corner cases
|
||||||
|
# where modules are being torn down after install with easy_install
|
||||||
|
import gc # noqa
|
||||||
|
for manager in list(cls.figs.values()):
|
||||||
|
manager.canvas.mpl_disconnect(manager._cidgcf)
|
||||||
|
manager.destroy()
|
||||||
|
|
||||||
|
cls._activeQue = []
|
||||||
|
cls.figs.clear()
|
||||||
|
gc.collect(1)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def has_fignum(cls, num):
|
||||||
|
"""
|
||||||
|
Return *True* if figure *num* exists.
|
||||||
|
"""
|
||||||
|
return num in cls.figs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_all_fig_managers(cls):
|
||||||
|
"""
|
||||||
|
Return a list of figure managers.
|
||||||
|
"""
|
||||||
|
return list(cls.figs.values())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_num_fig_managers(cls):
|
||||||
|
"""
|
||||||
|
Return the number of figures being managed.
|
||||||
|
"""
|
||||||
|
return len(cls.figs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_active(cls):
|
||||||
|
"""
|
||||||
|
Return the manager of the active figure, or *None*.
|
||||||
|
"""
|
||||||
|
if len(cls._activeQue) == 0:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return cls._activeQue[-1]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def set_active(cls, manager):
|
||||||
|
"""
|
||||||
|
Make the figure corresponding to *manager* the active one.
|
||||||
|
"""
|
||||||
|
oldQue = cls._activeQue[:]
|
||||||
|
cls._activeQue = []
|
||||||
|
for m in oldQue:
|
||||||
|
if m != manager:
|
||||||
|
cls._activeQue.append(m)
|
||||||
|
cls._activeQue.append(manager)
|
||||||
|
cls.figs[manager.num] = manager
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def draw_all(cls, force=False):
|
||||||
|
"""
|
||||||
|
Redraw all figures registered with the pyplot
|
||||||
|
state machine.
|
||||||
|
"""
|
||||||
|
for f_mgr in cls.get_all_fig_managers():
|
||||||
|
if force or f_mgr.canvas.figure.stale:
|
||||||
|
f_mgr.canvas.draw_idle()
|
||||||
|
|
||||||
|
atexit.register(Gcf.destroy_all)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
# This file was generated by 'versioneer.py' (0.15) from
|
||||||
|
# revision-control system data, or from the parent directory name of an
|
||||||
|
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
||||||
|
# of this file.
|
||||||
|
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
version_json = '''
|
||||||
|
{
|
||||||
|
"dirty": false,
|
||||||
|
"error": null,
|
||||||
|
"full-revisionid": "8858a0d1bdd149a0897789e8503ac586be14676d",
|
||||||
|
"version": "3.0.2"
|
||||||
|
}
|
||||||
|
''' # END VERSION_JSON
|
||||||
|
|
||||||
|
|
||||||
|
def get_versions():
|
||||||
|
return json.loads(version_json)
|
||||||
@@ -0,0 +1,562 @@
|
|||||||
|
"""
|
||||||
|
This is a python interface to Adobe Font Metrics Files. Although a
|
||||||
|
number of other python implementations exist, and may be more complete
|
||||||
|
than this, it was decided not to go with them because they were
|
||||||
|
either:
|
||||||
|
|
||||||
|
1) copyrighted or used a non-BSD compatible license
|
||||||
|
|
||||||
|
2) had too many dependencies and a free standing lib was needed
|
||||||
|
|
||||||
|
3) Did more than needed and it was easier to write afresh rather than
|
||||||
|
figure out how to get just what was needed.
|
||||||
|
|
||||||
|
It is pretty easy to use, and requires only built-in python libs:
|
||||||
|
|
||||||
|
>>> from matplotlib import rcParams
|
||||||
|
>>> import os.path
|
||||||
|
>>> afm_fname = os.path.join(rcParams['datapath'],
|
||||||
|
... 'fonts', 'afm', 'ptmr8a.afm')
|
||||||
|
>>>
|
||||||
|
>>> from matplotlib.afm import AFM
|
||||||
|
>>> with open(afm_fname, 'rb') as fh:
|
||||||
|
... afm = AFM(fh)
|
||||||
|
>>> afm.string_width_height('What the heck?')
|
||||||
|
(6220.0, 694)
|
||||||
|
>>> afm.get_fontname()
|
||||||
|
'Times-Roman'
|
||||||
|
>>> afm.get_kern_dist('A', 'f')
|
||||||
|
0
|
||||||
|
>>> afm.get_kern_dist('A', 'y')
|
||||||
|
-92.0
|
||||||
|
>>> afm.get_bbox_char('!')
|
||||||
|
[130, -9, 238, 676]
|
||||||
|
|
||||||
|
As in the Adobe Font Metrics File Format Specification, all dimensions
|
||||||
|
are given in units of 1/1000 of the scale factor (point size) of the font
|
||||||
|
being used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ._mathtext_data import uni2type1
|
||||||
|
from matplotlib.cbook import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
# some afm files have floats where we are expecting ints -- there is
|
||||||
|
# probably a better way to handle this (support floats, round rather
|
||||||
|
# than truncate). But I don't know what the best approach is now and
|
||||||
|
# this change to _to_int should at least prevent mpl from crashing on
|
||||||
|
# these JDH (2009-11-06)
|
||||||
|
|
||||||
|
def _to_int(x):
|
||||||
|
return int(float(x))
|
||||||
|
|
||||||
|
|
||||||
|
_to_float = float
|
||||||
|
|
||||||
|
|
||||||
|
def _to_str(x):
|
||||||
|
return x.decode('utf8')
|
||||||
|
|
||||||
|
|
||||||
|
def _to_list_of_ints(s):
|
||||||
|
s = s.replace(b',', b' ')
|
||||||
|
return [_to_int(val) for val in s.split()]
|
||||||
|
|
||||||
|
|
||||||
|
def _to_list_of_floats(s):
|
||||||
|
return [_to_float(val) for val in s.split()]
|
||||||
|
|
||||||
|
|
||||||
|
def _to_bool(s):
|
||||||
|
if s.lower().strip() in (b'false', b'0', b'no'):
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _sanity_check(fh):
|
||||||
|
"""
|
||||||
|
Check if the file at least looks like AFM.
|
||||||
|
If not, raise :exc:`RuntimeError`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Remember the file position in case the caller wants to
|
||||||
|
# do something else with the file.
|
||||||
|
pos = fh.tell()
|
||||||
|
try:
|
||||||
|
line = next(fh)
|
||||||
|
finally:
|
||||||
|
fh.seek(pos, 0)
|
||||||
|
|
||||||
|
# AFM spec, Section 4: The StartFontMetrics keyword [followed by a
|
||||||
|
# version number] must be the first line in the file, and the
|
||||||
|
# EndFontMetrics keyword must be the last non-empty line in the
|
||||||
|
# file. We just check the first line.
|
||||||
|
if not line.startswith(b'StartFontMetrics'):
|
||||||
|
raise RuntimeError('Not an AFM file')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_header(fh):
|
||||||
|
"""
|
||||||
|
Reads the font metrics header (up to the char metrics) and returns
|
||||||
|
a dictionary mapping *key* to *val*. *val* will be converted to the
|
||||||
|
appropriate python type as necessary; e.g.:
|
||||||
|
|
||||||
|
* 'False'->False
|
||||||
|
* '0'->0
|
||||||
|
* '-168 -218 1000 898'-> [-168, -218, 1000, 898]
|
||||||
|
|
||||||
|
Dictionary keys are
|
||||||
|
|
||||||
|
StartFontMetrics, FontName, FullName, FamilyName, Weight,
|
||||||
|
ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition,
|
||||||
|
UnderlineThickness, Version, Notice, EncodingScheme, CapHeight,
|
||||||
|
XHeight, Ascender, Descender, StartCharMetrics
|
||||||
|
|
||||||
|
"""
|
||||||
|
headerConverters = {
|
||||||
|
b'StartFontMetrics': _to_float,
|
||||||
|
b'FontName': _to_str,
|
||||||
|
b'FullName': _to_str,
|
||||||
|
b'FamilyName': _to_str,
|
||||||
|
b'Weight': _to_str,
|
||||||
|
b'ItalicAngle': _to_float,
|
||||||
|
b'IsFixedPitch': _to_bool,
|
||||||
|
b'FontBBox': _to_list_of_ints,
|
||||||
|
b'UnderlinePosition': _to_int,
|
||||||
|
b'UnderlineThickness': _to_int,
|
||||||
|
b'Version': _to_str,
|
||||||
|
b'Notice': _to_str,
|
||||||
|
b'EncodingScheme': _to_str,
|
||||||
|
b'CapHeight': _to_float, # Is the second version a mistake, or
|
||||||
|
b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS
|
||||||
|
b'XHeight': _to_float,
|
||||||
|
b'Ascender': _to_float,
|
||||||
|
b'Descender': _to_float,
|
||||||
|
b'StdHW': _to_float,
|
||||||
|
b'StdVW': _to_float,
|
||||||
|
b'StartCharMetrics': _to_int,
|
||||||
|
b'CharacterSet': _to_str,
|
||||||
|
b'Characters': _to_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
d = {}
|
||||||
|
for line in fh:
|
||||||
|
line = line.rstrip()
|
||||||
|
if line.startswith(b'Comment'):
|
||||||
|
continue
|
||||||
|
lst = line.split(b' ', 1)
|
||||||
|
|
||||||
|
key = lst[0]
|
||||||
|
if len(lst) == 2:
|
||||||
|
val = lst[1]
|
||||||
|
else:
|
||||||
|
val = b''
|
||||||
|
|
||||||
|
try:
|
||||||
|
d[key] = headerConverters[key](val)
|
||||||
|
except ValueError:
|
||||||
|
print('Value error parsing header in AFM:', key, val,
|
||||||
|
file=sys.stderr)
|
||||||
|
continue
|
||||||
|
except KeyError:
|
||||||
|
print('Found an unknown keyword in AFM header (was %r)' % key,
|
||||||
|
file=sys.stderr)
|
||||||
|
continue
|
||||||
|
if key == b'StartCharMetrics':
|
||||||
|
return d
|
||||||
|
raise RuntimeError('Bad parse')
|
||||||
|
|
||||||
|
|
||||||
|
CharMetrics = namedtuple('CharMetrics', 'width, name, bbox')
|
||||||
|
CharMetrics.__doc__ = """
|
||||||
|
Represents the character metrics of a single character.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
The fields do currently only describe a subset of character metrics
|
||||||
|
information defined in the AFM standard.
|
||||||
|
"""
|
||||||
|
CharMetrics.width.__doc__ = """The character width (WX)."""
|
||||||
|
CharMetrics.name.__doc__ = """The character name (N)."""
|
||||||
|
CharMetrics.bbox.__doc__ = """
|
||||||
|
The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*)."""
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_char_metrics(fh):
|
||||||
|
"""
|
||||||
|
Parse the given filehandle for character metrics information and return
|
||||||
|
the information as dicts.
|
||||||
|
|
||||||
|
It is assumed that the file cursor is on the line behind
|
||||||
|
'StartCharMetrics'.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
ascii_d : dict
|
||||||
|
A mapping "ASCII num of the character" to `.CharMetrics`.
|
||||||
|
name_d : dict
|
||||||
|
A mapping "character name" to `.CharMetrics`.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
This function is incomplete per the standard, but thus far parses
|
||||||
|
all the sample afm files tried.
|
||||||
|
"""
|
||||||
|
required_keys = {'C', 'WX', 'N', 'B'}
|
||||||
|
|
||||||
|
ascii_d = {}
|
||||||
|
name_d = {}
|
||||||
|
for line in fh:
|
||||||
|
# We are defensively letting values be utf8. The spec requires
|
||||||
|
# ascii, but there are non-compliant fonts in circulation
|
||||||
|
line = _to_str(line.rstrip()) # Convert from byte-literal
|
||||||
|
if line.startswith('EndCharMetrics'):
|
||||||
|
return ascii_d, name_d
|
||||||
|
# Split the metric line into a dictionary, keyed by metric identifiers
|
||||||
|
vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s)
|
||||||
|
# There may be other metrics present, but only these are needed
|
||||||
|
if not required_keys.issubset(vals):
|
||||||
|
raise RuntimeError('Bad char metrics line: %s' % line)
|
||||||
|
num = _to_int(vals['C'])
|
||||||
|
wx = _to_float(vals['WX'])
|
||||||
|
name = vals['N']
|
||||||
|
bbox = _to_list_of_floats(vals['B'])
|
||||||
|
bbox = list(map(int, bbox))
|
||||||
|
metrics = CharMetrics(wx, name, bbox)
|
||||||
|
# Workaround: If the character name is 'Euro', give it the
|
||||||
|
# corresponding character code, according to WinAnsiEncoding (see PDF
|
||||||
|
# Reference).
|
||||||
|
if name == 'Euro':
|
||||||
|
num = 128
|
||||||
|
if num != -1:
|
||||||
|
ascii_d[num] = metrics
|
||||||
|
name_d[name] = metrics
|
||||||
|
raise RuntimeError('Bad parse')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_kern_pairs(fh):
|
||||||
|
"""
|
||||||
|
Return a kern pairs dictionary; keys are (*char1*, *char2*) tuples and
|
||||||
|
values are the kern pair value. For example, a kern pairs line like
|
||||||
|
``KPX A y -50``
|
||||||
|
|
||||||
|
will be represented as::
|
||||||
|
|
||||||
|
d[ ('A', 'y') ] = -50
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
line = next(fh)
|
||||||
|
if not line.startswith(b'StartKernPairs'):
|
||||||
|
raise RuntimeError('Bad start of kern pairs data: %s' % line)
|
||||||
|
|
||||||
|
d = {}
|
||||||
|
for line in fh:
|
||||||
|
line = line.rstrip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.startswith(b'EndKernPairs'):
|
||||||
|
next(fh) # EndKernData
|
||||||
|
return d
|
||||||
|
vals = line.split()
|
||||||
|
if len(vals) != 4 or vals[0] != b'KPX':
|
||||||
|
raise RuntimeError('Bad kern pairs line: %s' % line)
|
||||||
|
c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3])
|
||||||
|
d[(c1, c2)] = val
|
||||||
|
raise RuntimeError('Bad kern pairs parse')
|
||||||
|
|
||||||
|
|
||||||
|
CompositePart = namedtuple('CompositePart', 'name, dx, dy')
|
||||||
|
CompositePart.__doc__ = """
|
||||||
|
Represents the information on a composite element of a composite char."""
|
||||||
|
CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'."""
|
||||||
|
CompositePart.dx.__doc__ = """x-displacement of the part from the origin."""
|
||||||
|
CompositePart.dy.__doc__ = """y-displacement of the part from the origin."""
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_composites(fh):
|
||||||
|
"""
|
||||||
|
Parse the given filehandle for composites information return them as a
|
||||||
|
dict.
|
||||||
|
|
||||||
|
It is assumed that the file cursor is on the line behind 'StartComposites'.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
composites : dict
|
||||||
|
A dict mapping composite character names to a parts list. The parts
|
||||||
|
list is a list of `.CompositePart` entries describing the parts of
|
||||||
|
the composite.
|
||||||
|
|
||||||
|
Example
|
||||||
|
-------
|
||||||
|
A composite definition line::
|
||||||
|
|
||||||
|
CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ;
|
||||||
|
|
||||||
|
will be represented as::
|
||||||
|
|
||||||
|
composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0),
|
||||||
|
CompositePart(name='acute', dx=160, dy=170)]
|
||||||
|
|
||||||
|
"""
|
||||||
|
composites = {}
|
||||||
|
for line in fh:
|
||||||
|
line = line.rstrip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
if line.startswith(b'EndComposites'):
|
||||||
|
return composites
|
||||||
|
vals = line.split(b';')
|
||||||
|
cc = vals[0].split()
|
||||||
|
name, numParts = cc[1], _to_int(cc[2])
|
||||||
|
pccParts = []
|
||||||
|
for s in vals[1:-1]:
|
||||||
|
pcc = s.split()
|
||||||
|
part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3]))
|
||||||
|
pccParts.append(part)
|
||||||
|
composites[name] = pccParts
|
||||||
|
|
||||||
|
raise RuntimeError('Bad composites parse')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_optional(fh):
|
||||||
|
"""
|
||||||
|
Parse the optional fields for kern pair data and composites.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
kern_data : dict
|
||||||
|
A dict containing kerning information. May be empty.
|
||||||
|
See `._parse_kern_pairs`.
|
||||||
|
composites : dict
|
||||||
|
A dict containing composite information. May be empty.
|
||||||
|
See `._parse_composites`.
|
||||||
|
"""
|
||||||
|
optional = {
|
||||||
|
b'StartKernData': _parse_kern_pairs,
|
||||||
|
b'StartComposites': _parse_composites,
|
||||||
|
}
|
||||||
|
|
||||||
|
d = {b'StartKernData': {},
|
||||||
|
b'StartComposites': {}}
|
||||||
|
for line in fh:
|
||||||
|
line = line.rstrip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
key = line.split()[0]
|
||||||
|
|
||||||
|
if key in optional:
|
||||||
|
d[key] = optional[key](fh)
|
||||||
|
|
||||||
|
return d[b'StartKernData'], d[b'StartComposites']
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("3.0", "Use the class AFM instead.")
|
||||||
|
def parse_afm(fh):
|
||||||
|
return _parse_afm(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_afm(fh):
|
||||||
|
"""
|
||||||
|
Parse the Adobe Font Metrics file in file handle *fh*.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
header : dict
|
||||||
|
A header dict. See :func:`_parse_header`.
|
||||||
|
cmetrics_by_ascii : dict
|
||||||
|
From :func:`_parse_char_metrics`.
|
||||||
|
cmetrics_by_name : dict
|
||||||
|
From :func:`_parse_char_metrics`.
|
||||||
|
kernpairs : dict
|
||||||
|
From :func:`_parse_kern_pairs`.
|
||||||
|
composites : dict
|
||||||
|
From :func:`_parse_composites`
|
||||||
|
|
||||||
|
"""
|
||||||
|
_sanity_check(fh)
|
||||||
|
header = _parse_header(fh)
|
||||||
|
cmetrics_by_ascii, cmetrics_by_name = _parse_char_metrics(fh)
|
||||||
|
kernpairs, composites = _parse_optional(fh)
|
||||||
|
return header, cmetrics_by_ascii, cmetrics_by_name, kernpairs, composites
|
||||||
|
|
||||||
|
|
||||||
|
class AFM(object):
|
||||||
|
|
||||||
|
def __init__(self, fh):
|
||||||
|
"""Parse the AFM file in file object *fh*."""
|
||||||
|
(self._header,
|
||||||
|
self._metrics,
|
||||||
|
self._metrics_by_name,
|
||||||
|
self._kern,
|
||||||
|
self._composite) = _parse_afm(fh)
|
||||||
|
|
||||||
|
def get_bbox_char(self, c, isord=False):
|
||||||
|
if not isord:
|
||||||
|
c = ord(c)
|
||||||
|
return self._metrics[c].bbox
|
||||||
|
|
||||||
|
def string_width_height(self, s):
|
||||||
|
"""
|
||||||
|
Return the string width (including kerning) and string height
|
||||||
|
as a (*w*, *h*) tuple.
|
||||||
|
"""
|
||||||
|
if not len(s):
|
||||||
|
return 0, 0
|
||||||
|
total_width = 0
|
||||||
|
namelast = None
|
||||||
|
miny = 1e9
|
||||||
|
maxy = 0
|
||||||
|
for c in s:
|
||||||
|
if c == '\n':
|
||||||
|
continue
|
||||||
|
wx, name, bbox = self._metrics[ord(c)]
|
||||||
|
|
||||||
|
total_width += wx + self._kern.get((namelast, name), 0)
|
||||||
|
l, b, w, h = bbox
|
||||||
|
miny = min(miny, b)
|
||||||
|
maxy = max(maxy, b + h)
|
||||||
|
|
||||||
|
namelast = name
|
||||||
|
|
||||||
|
return total_width, maxy - miny
|
||||||
|
|
||||||
|
def get_str_bbox_and_descent(self, s):
|
||||||
|
"""Return the string bounding box and the maximal descent."""
|
||||||
|
if not len(s):
|
||||||
|
return 0, 0, 0, 0, 0
|
||||||
|
total_width = 0
|
||||||
|
namelast = None
|
||||||
|
miny = 1e9
|
||||||
|
maxy = 0
|
||||||
|
left = 0
|
||||||
|
if not isinstance(s, str):
|
||||||
|
s = _to_str(s)
|
||||||
|
for c in s:
|
||||||
|
if c == '\n':
|
||||||
|
continue
|
||||||
|
name = uni2type1.get(ord(c), 'question')
|
||||||
|
try:
|
||||||
|
wx, _, bbox = self._metrics_by_name[name]
|
||||||
|
except KeyError:
|
||||||
|
name = 'question'
|
||||||
|
wx, _, bbox = self._metrics_by_name[name]
|
||||||
|
total_width += wx + self._kern.get((namelast, name), 0)
|
||||||
|
l, b, w, h = bbox
|
||||||
|
left = min(left, l)
|
||||||
|
miny = min(miny, b)
|
||||||
|
maxy = max(maxy, b + h)
|
||||||
|
|
||||||
|
namelast = name
|
||||||
|
|
||||||
|
return left, miny, total_width, maxy - miny, -miny
|
||||||
|
|
||||||
|
def get_str_bbox(self, s):
|
||||||
|
"""Return the string bounding box."""
|
||||||
|
return self.get_str_bbox_and_descent(s)[:4]
|
||||||
|
|
||||||
|
def get_name_char(self, c, isord=False):
|
||||||
|
"""Get the name of the character, i.e., ';' is 'semicolon'."""
|
||||||
|
if not isord:
|
||||||
|
c = ord(c)
|
||||||
|
return self._metrics[c].name
|
||||||
|
|
||||||
|
def get_width_char(self, c, isord=False):
|
||||||
|
"""
|
||||||
|
Get the width of the character from the character metric WX field.
|
||||||
|
"""
|
||||||
|
if not isord:
|
||||||
|
c = ord(c)
|
||||||
|
return self._metrics[c].width
|
||||||
|
|
||||||
|
def get_width_from_char_name(self, name):
|
||||||
|
"""Get the width of the character from a type1 character name."""
|
||||||
|
return self._metrics_by_name[name].width
|
||||||
|
|
||||||
|
def get_height_char(self, c, isord=False):
|
||||||
|
"""Get the bounding box (ink) height of character *c* (space is 0)."""
|
||||||
|
if not isord:
|
||||||
|
c = ord(c)
|
||||||
|
return self._metrics[c].bbox[-1]
|
||||||
|
|
||||||
|
def get_kern_dist(self, c1, c2):
|
||||||
|
"""
|
||||||
|
Return the kerning pair distance (possibly 0) for chars *c1* and *c2*.
|
||||||
|
"""
|
||||||
|
name1, name2 = self.get_name_char(c1), self.get_name_char(c2)
|
||||||
|
return self.get_kern_dist_from_name(name1, name2)
|
||||||
|
|
||||||
|
def get_kern_dist_from_name(self, name1, name2):
|
||||||
|
"""
|
||||||
|
Return the kerning pair distance (possibly 0) for chars
|
||||||
|
*name1* and *name2*.
|
||||||
|
"""
|
||||||
|
return self._kern.get((name1, name2), 0)
|
||||||
|
|
||||||
|
def get_fontname(self):
|
||||||
|
"""Return the font name, e.g., 'Times-Roman'."""
|
||||||
|
return self._header[b'FontName']
|
||||||
|
|
||||||
|
def get_fullname(self):
|
||||||
|
"""Return the font full name, e.g., 'Times-Roman'."""
|
||||||
|
name = self._header.get(b'FullName')
|
||||||
|
if name is None: # use FontName as a substitute
|
||||||
|
name = self._header[b'FontName']
|
||||||
|
return name
|
||||||
|
|
||||||
|
def get_familyname(self):
|
||||||
|
"""Return the font family name, e.g., 'Times'."""
|
||||||
|
name = self._header.get(b'FamilyName')
|
||||||
|
if name is not None:
|
||||||
|
return name
|
||||||
|
|
||||||
|
# FamilyName not specified so we'll make a guess
|
||||||
|
name = self.get_fullname()
|
||||||
|
extras = (r'(?i)([ -](regular|plain|italic|oblique|bold|semibold|'
|
||||||
|
r'light|ultralight|extra|condensed))+$')
|
||||||
|
return re.sub(extras, '', name)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def family_name(self):
|
||||||
|
"""The font family name, e.g., 'Times'."""
|
||||||
|
return self.get_familyname()
|
||||||
|
|
||||||
|
def get_weight(self):
|
||||||
|
"""Return the font weight, e.g., 'Bold' or 'Roman'."""
|
||||||
|
return self._header[b'Weight']
|
||||||
|
|
||||||
|
def get_angle(self):
|
||||||
|
"""Return the fontangle as float."""
|
||||||
|
return self._header[b'ItalicAngle']
|
||||||
|
|
||||||
|
def get_capheight(self):
|
||||||
|
"""Return the cap height as float."""
|
||||||
|
return self._header[b'CapHeight']
|
||||||
|
|
||||||
|
def get_xheight(self):
|
||||||
|
"""Return the xheight as float."""
|
||||||
|
return self._header[b'XHeight']
|
||||||
|
|
||||||
|
def get_underline_thickness(self):
|
||||||
|
"""Return the underline thickness as float."""
|
||||||
|
return self._header[b'UnderlineThickness']
|
||||||
|
|
||||||
|
def get_horizontal_stem_width(self):
|
||||||
|
"""
|
||||||
|
Return the standard horizontal stem width as float, or *None* if
|
||||||
|
not specified in AFM file.
|
||||||
|
"""
|
||||||
|
return self._header.get(b'StdHW', None)
|
||||||
|
|
||||||
|
def get_vertical_stem_width(self):
|
||||||
|
"""
|
||||||
|
Return the standard vertical stem width as float, or *None* if
|
||||||
|
not specified in AFM file.
|
||||||
|
"""
|
||||||
|
return self._header.get(b'StdVW', None)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from ._subplots import *
|
||||||
|
from ._axes import *
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import functools
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
from matplotlib import docstring
|
||||||
|
import matplotlib.artist as martist
|
||||||
|
from matplotlib.axes._axes import Axes
|
||||||
|
from matplotlib.gridspec import GridSpec, SubplotSpec
|
||||||
|
import matplotlib._layoutbox as layoutbox
|
||||||
|
|
||||||
|
|
||||||
|
class SubplotBase(object):
|
||||||
|
"""
|
||||||
|
Base class for subplots, which are :class:`Axes` instances with
|
||||||
|
additional methods to facilitate generating and manipulating a set
|
||||||
|
of :class:`Axes` within a figure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, fig, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
*fig* is a :class:`matplotlib.figure.Figure` instance.
|
||||||
|
|
||||||
|
*args* is the tuple (*numRows*, *numCols*, *plotNum*), where
|
||||||
|
the array of subplots in the figure has dimensions *numRows*,
|
||||||
|
*numCols*, and where *plotNum* is the number of the subplot
|
||||||
|
being created. *plotNum* starts at 1 in the upper left
|
||||||
|
corner and increases to the right.
|
||||||
|
|
||||||
|
If *numRows* <= *numCols* <= *plotNum* < 10, *args* can be the
|
||||||
|
decimal integer *numRows* * 100 + *numCols* * 10 + *plotNum*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.figure = fig
|
||||||
|
|
||||||
|
if len(args) == 1:
|
||||||
|
if isinstance(args[0], SubplotSpec):
|
||||||
|
self._subplotspec = args[0]
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
s = str(int(args[0]))
|
||||||
|
rows, cols, num = map(int, s)
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError('Single argument to subplot must be '
|
||||||
|
'a 3-digit integer')
|
||||||
|
self._subplotspec = GridSpec(rows, cols,
|
||||||
|
figure=self.figure)[num - 1]
|
||||||
|
# num - 1 for converting from MATLAB to python indexing
|
||||||
|
elif len(args) == 3:
|
||||||
|
rows, cols, num = args
|
||||||
|
rows = int(rows)
|
||||||
|
cols = int(cols)
|
||||||
|
if isinstance(num, tuple) and len(num) == 2:
|
||||||
|
num = [int(n) for n in num]
|
||||||
|
self._subplotspec = GridSpec(
|
||||||
|
rows, cols,
|
||||||
|
figure=self.figure)[(num[0] - 1):num[1]]
|
||||||
|
else:
|
||||||
|
if num < 1 or num > rows*cols:
|
||||||
|
raise ValueError(
|
||||||
|
("num must be 1 <= num <= {maxn}, not {num}"
|
||||||
|
).format(maxn=rows*cols, num=num))
|
||||||
|
self._subplotspec = GridSpec(
|
||||||
|
rows, cols, figure=self.figure)[int(num) - 1]
|
||||||
|
# num - 1 for converting from MATLAB to python indexing
|
||||||
|
else:
|
||||||
|
raise ValueError('Illegal argument(s) to subplot: %s' % (args,))
|
||||||
|
|
||||||
|
self.update_params()
|
||||||
|
|
||||||
|
# _axes_class is set in the subplot_class_factory
|
||||||
|
self._axes_class.__init__(self, fig, self.figbox, **kwargs)
|
||||||
|
# add a layout box to this, for both the full axis, and the poss
|
||||||
|
# of the axis. We need both because the axes may become smaller
|
||||||
|
# due to parasitic axes and hence no longer fill the subplotspec.
|
||||||
|
if self._subplotspec._layoutbox is None:
|
||||||
|
self._layoutbox = None
|
||||||
|
self._poslayoutbox = None
|
||||||
|
else:
|
||||||
|
name = self._subplotspec._layoutbox.name + '.ax'
|
||||||
|
name = name + layoutbox.seq_id()
|
||||||
|
self._layoutbox = layoutbox.LayoutBox(
|
||||||
|
parent=self._subplotspec._layoutbox,
|
||||||
|
name=name,
|
||||||
|
artist=self)
|
||||||
|
self._poslayoutbox = layoutbox.LayoutBox(
|
||||||
|
parent=self._layoutbox,
|
||||||
|
name=self._layoutbox.name+'.pos',
|
||||||
|
pos=True, subplot=True, artist=self)
|
||||||
|
|
||||||
|
def __reduce__(self):
|
||||||
|
# get the first axes class which does not inherit from a subplotbase
|
||||||
|
axes_class = next(
|
||||||
|
c for c in type(self).__mro__
|
||||||
|
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
|
||||||
|
return (_picklable_subplot_class_constructor,
|
||||||
|
(axes_class,),
|
||||||
|
self.__getstate__())
|
||||||
|
|
||||||
|
def get_geometry(self):
|
||||||
|
"""get the subplot geometry, e.g., 2,2,3"""
|
||||||
|
rows, cols, num1, num2 = self.get_subplotspec().get_geometry()
|
||||||
|
return rows, cols, num1 + 1 # for compatibility
|
||||||
|
|
||||||
|
# COVERAGE NOTE: Never used internally or from examples
|
||||||
|
def change_geometry(self, numrows, numcols, num):
|
||||||
|
"""change subplot geometry, e.g., from 1,1,1 to 2,2,3"""
|
||||||
|
self._subplotspec = GridSpec(numrows, numcols,
|
||||||
|
figure=self.figure)[num - 1]
|
||||||
|
self.update_params()
|
||||||
|
self.set_position(self.figbox)
|
||||||
|
|
||||||
|
def get_subplotspec(self):
|
||||||
|
"""get the SubplotSpec instance associated with the subplot"""
|
||||||
|
return self._subplotspec
|
||||||
|
|
||||||
|
def set_subplotspec(self, subplotspec):
|
||||||
|
"""set the SubplotSpec instance associated with the subplot"""
|
||||||
|
self._subplotspec = subplotspec
|
||||||
|
|
||||||
|
def get_gridspec(self):
|
||||||
|
"""get the GridSpec instance associated with the subplot"""
|
||||||
|
return self._subplotspec.get_gridspec()
|
||||||
|
|
||||||
|
def update_params(self):
|
||||||
|
"""update the subplot position from fig.subplotpars"""
|
||||||
|
|
||||||
|
self.figbox, self.rowNum, self.colNum, self.numRows, self.numCols = \
|
||||||
|
self.get_subplotspec().get_position(self.figure,
|
||||||
|
return_all=True)
|
||||||
|
|
||||||
|
def is_first_col(self):
|
||||||
|
return self.colNum == 0
|
||||||
|
|
||||||
|
def is_first_row(self):
|
||||||
|
return self.rowNum == 0
|
||||||
|
|
||||||
|
def is_last_row(self):
|
||||||
|
return self.rowNum == self.numRows - 1
|
||||||
|
|
||||||
|
def is_last_col(self):
|
||||||
|
return self.colNum == self.numCols - 1
|
||||||
|
|
||||||
|
# COVERAGE NOTE: Never used internally.
|
||||||
|
def label_outer(self):
|
||||||
|
"""Only show "outer" labels and tick labels.
|
||||||
|
|
||||||
|
x-labels are only kept for subplots on the last row; y-labels only for
|
||||||
|
subplots on the first column.
|
||||||
|
"""
|
||||||
|
lastrow = self.is_last_row()
|
||||||
|
firstcol = self.is_first_col()
|
||||||
|
if not lastrow:
|
||||||
|
for label in self.get_xticklabels(which="both"):
|
||||||
|
label.set_visible(False)
|
||||||
|
self.get_xaxis().get_offset_text().set_visible(False)
|
||||||
|
self.set_xlabel("")
|
||||||
|
if not firstcol:
|
||||||
|
for label in self.get_yticklabels(which="both"):
|
||||||
|
label.set_visible(False)
|
||||||
|
self.get_yaxis().get_offset_text().set_visible(False)
|
||||||
|
self.set_ylabel("")
|
||||||
|
|
||||||
|
def _make_twin_axes(self, *kl, **kwargs):
|
||||||
|
"""
|
||||||
|
Make a twinx axes of self. This is used for twinx and twiny.
|
||||||
|
"""
|
||||||
|
from matplotlib.projections import process_projection_requirements
|
||||||
|
if 'sharex' in kwargs and 'sharey' in kwargs:
|
||||||
|
# The following line is added in v2.2 to avoid breaking Seaborn,
|
||||||
|
# which currently uses this internal API.
|
||||||
|
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
|
||||||
|
raise ValueError("Twinned Axes may share only one axis.")
|
||||||
|
kl = (self.get_subplotspec(),) + kl
|
||||||
|
projection_class, kwargs, key = process_projection_requirements(
|
||||||
|
self.figure, *kl, **kwargs)
|
||||||
|
|
||||||
|
ax2 = subplot_class_factory(projection_class)(self.figure,
|
||||||
|
*kl, **kwargs)
|
||||||
|
self.figure.add_subplot(ax2)
|
||||||
|
self.set_adjustable('datalim')
|
||||||
|
ax2.set_adjustable('datalim')
|
||||||
|
|
||||||
|
if self._layoutbox is not None and ax2._layoutbox is not None:
|
||||||
|
# make the layout boxes be explicitly the same
|
||||||
|
ax2._layoutbox.constrain_same(self._layoutbox)
|
||||||
|
ax2._poslayoutbox.constrain_same(self._poslayoutbox)
|
||||||
|
|
||||||
|
self._twinned_axes.join(self, ax2)
|
||||||
|
return ax2
|
||||||
|
|
||||||
|
|
||||||
|
# this here to support cartopy which was using a private part of the
|
||||||
|
# API to register their Axes subclasses.
|
||||||
|
|
||||||
|
# In 3.1 this should be changed to a dict subclass that warns on use
|
||||||
|
# In 3.3 to a dict subclass that raises a useful exception on use
|
||||||
|
# In 3.4 should be removed
|
||||||
|
|
||||||
|
# The slow timeline is to give cartopy enough time to get several
|
||||||
|
# release out before we break them.
|
||||||
|
_subplot_classes = {}
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(None)
|
||||||
|
def subplot_class_factory(axes_class=None):
|
||||||
|
"""
|
||||||
|
This makes a new class that inherits from `.SubplotBase` and the
|
||||||
|
given axes_class (which is assumed to be a subclass of `.axes.Axes`).
|
||||||
|
This is perhaps a little bit roundabout to make a new class on
|
||||||
|
the fly like this, but it means that a new Subplot class does
|
||||||
|
not have to be created for every type of Axes.
|
||||||
|
"""
|
||||||
|
if axes_class is None:
|
||||||
|
axes_class = Axes
|
||||||
|
try:
|
||||||
|
# Avoid creating two different instances of GeoAxesSubplot...
|
||||||
|
# Only a temporary backcompat fix. This should be removed in
|
||||||
|
# 3.4
|
||||||
|
return next(cls for cls in SubplotBase.__subclasses__()
|
||||||
|
if cls.__bases__ == (SubplotBase, axes_class))
|
||||||
|
except StopIteration:
|
||||||
|
return type("%sSubplot" % axes_class.__name__,
|
||||||
|
(SubplotBase, axes_class),
|
||||||
|
{'_axes_class': axes_class})
|
||||||
|
|
||||||
|
|
||||||
|
# This is provided for backward compatibility
|
||||||
|
Subplot = subplot_class_factory()
|
||||||
|
|
||||||
|
|
||||||
|
def _picklable_subplot_class_constructor(axes_class):
|
||||||
|
"""
|
||||||
|
This stub class exists to return the appropriate subplot class when called
|
||||||
|
with an axes class. This is purely to allow pickling of Axes and Subplots.
|
||||||
|
"""
|
||||||
|
subplot_class = subplot_class_factory(axes_class)
|
||||||
|
return subplot_class.__new__(subplot_class)
|
||||||
|
|
||||||
|
|
||||||
|
docstring.interpd.update(Axes=martist.kwdoc(Axes))
|
||||||
|
docstring.dedent_interpd(Axes.__init__)
|
||||||
|
|
||||||
|
docstring.interpd.update(Subplot=martist.kwdoc(Axes))
|
||||||
@@ -0,0 +1,433 @@
|
|||||||
|
"""
|
||||||
|
`ToolManager`
|
||||||
|
Class that makes the bridge between user interaction (key press,
|
||||||
|
toolbar clicks, ..) and the actions in response to the user inputs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
import matplotlib.cbook as cbook
|
||||||
|
import matplotlib.widgets as widgets
|
||||||
|
from matplotlib.rcsetup import validate_stringlist
|
||||||
|
import matplotlib.backend_tools as tools
|
||||||
|
|
||||||
|
|
||||||
|
class ToolEvent(object):
|
||||||
|
"""Event for tool manipulation (add/remove)"""
|
||||||
|
def __init__(self, name, sender, tool, data=None):
|
||||||
|
self.name = name
|
||||||
|
self.sender = sender
|
||||||
|
self.tool = tool
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
|
||||||
|
class ToolTriggerEvent(ToolEvent):
|
||||||
|
"""Event to inform that a tool has been triggered"""
|
||||||
|
def __init__(self, name, sender, tool, canvasevent=None, data=None):
|
||||||
|
ToolEvent.__init__(self, name, sender, tool, data)
|
||||||
|
self.canvasevent = canvasevent
|
||||||
|
|
||||||
|
|
||||||
|
class ToolManagerMessageEvent(object):
|
||||||
|
"""
|
||||||
|
Event carrying messages from toolmanager
|
||||||
|
|
||||||
|
Messages usually get displayed to the user by the toolbar
|
||||||
|
"""
|
||||||
|
def __init__(self, name, sender, message):
|
||||||
|
self.name = name
|
||||||
|
self.sender = sender
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
|
||||||
|
class ToolManager(object):
|
||||||
|
"""
|
||||||
|
Helper class that groups all the user interactions for a Figure.
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
figure: `Figure`
|
||||||
|
keypresslock: `widgets.LockDraw`
|
||||||
|
`LockDraw` object to know if the `canvas` key_press_event is locked
|
||||||
|
messagelock: `widgets.LockDraw`
|
||||||
|
`LockDraw` object to know if the message is available to write
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, figure=None):
|
||||||
|
warnings.warn('Treat the new Tool classes introduced in v1.5 as ' +
|
||||||
|
'experimental for now, the API will likely change in ' +
|
||||||
|
'version 2.1 and perhaps the rcParam as well')
|
||||||
|
|
||||||
|
self._key_press_handler_id = None
|
||||||
|
|
||||||
|
self._tools = {}
|
||||||
|
self._keys = {}
|
||||||
|
self._toggled = {}
|
||||||
|
self._callbacks = cbook.CallbackRegistry()
|
||||||
|
|
||||||
|
# to process keypress event
|
||||||
|
self.keypresslock = widgets.LockDraw()
|
||||||
|
self.messagelock = widgets.LockDraw()
|
||||||
|
|
||||||
|
self._figure = None
|
||||||
|
self.set_figure(figure)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def canvas(self):
|
||||||
|
"""Canvas managed by FigureManager"""
|
||||||
|
if not self._figure:
|
||||||
|
return None
|
||||||
|
return self._figure.canvas
|
||||||
|
|
||||||
|
@property
|
||||||
|
def figure(self):
|
||||||
|
"""Figure that holds the canvas"""
|
||||||
|
return self._figure
|
||||||
|
|
||||||
|
@figure.setter
|
||||||
|
def figure(self, figure):
|
||||||
|
self.set_figure(figure)
|
||||||
|
|
||||||
|
def set_figure(self, figure, update_tools=True):
|
||||||
|
"""
|
||||||
|
Bind the given figure to the tools.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
figure : `.Figure`
|
||||||
|
update_tools : bool
|
||||||
|
Force tools to update figure
|
||||||
|
"""
|
||||||
|
if self._key_press_handler_id:
|
||||||
|
self.canvas.mpl_disconnect(self._key_press_handler_id)
|
||||||
|
self._figure = figure
|
||||||
|
if figure:
|
||||||
|
self._key_press_handler_id = self.canvas.mpl_connect(
|
||||||
|
'key_press_event', self._key_press)
|
||||||
|
if update_tools:
|
||||||
|
for tool in self._tools.values():
|
||||||
|
tool.figure = figure
|
||||||
|
|
||||||
|
def toolmanager_connect(self, s, func):
|
||||||
|
"""
|
||||||
|
Connect event with string *s* to *func*.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
s : String
|
||||||
|
Name of the event
|
||||||
|
|
||||||
|
The following events are recognized
|
||||||
|
|
||||||
|
- 'tool_message_event'
|
||||||
|
- 'tool_removed_event'
|
||||||
|
- 'tool_added_event'
|
||||||
|
|
||||||
|
For every tool added a new event is created
|
||||||
|
|
||||||
|
- 'tool_trigger_TOOLNAME`
|
||||||
|
Where TOOLNAME is the id of the tool.
|
||||||
|
|
||||||
|
func : function
|
||||||
|
Function to be called with signature
|
||||||
|
def func(event)
|
||||||
|
"""
|
||||||
|
return self._callbacks.connect(s, func)
|
||||||
|
|
||||||
|
def toolmanager_disconnect(self, cid):
|
||||||
|
"""
|
||||||
|
Disconnect callback id *cid*
|
||||||
|
|
||||||
|
Example usage::
|
||||||
|
|
||||||
|
cid = toolmanager.toolmanager_connect('tool_trigger_zoom',
|
||||||
|
on_press)
|
||||||
|
#...later
|
||||||
|
toolmanager.toolmanager_disconnect(cid)
|
||||||
|
"""
|
||||||
|
return self._callbacks.disconnect(cid)
|
||||||
|
|
||||||
|
def message_event(self, message, sender=None):
|
||||||
|
""" Emit a `ToolManagerMessageEvent`"""
|
||||||
|
if sender is None:
|
||||||
|
sender = self
|
||||||
|
|
||||||
|
s = 'tool_message_event'
|
||||||
|
event = ToolManagerMessageEvent(s, sender, message)
|
||||||
|
self._callbacks.process(s, event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_toggle(self):
|
||||||
|
"""Currently toggled tools"""
|
||||||
|
|
||||||
|
return self._toggled
|
||||||
|
|
||||||
|
def get_tool_keymap(self, name):
|
||||||
|
"""
|
||||||
|
Get the keymap associated with the specified tool
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : string
|
||||||
|
Name of the Tool
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
list : list of keys associated with the Tool
|
||||||
|
"""
|
||||||
|
|
||||||
|
keys = [k for k, i in self._keys.items() if i == name]
|
||||||
|
return keys
|
||||||
|
|
||||||
|
def _remove_keys(self, name):
|
||||||
|
for k in self.get_tool_keymap(name):
|
||||||
|
del self._keys[k]
|
||||||
|
|
||||||
|
def update_keymap(self, name, *keys):
|
||||||
|
"""
|
||||||
|
Set the keymap to associate with the specified tool
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : string
|
||||||
|
Name of the Tool
|
||||||
|
keys : keys to associate with the Tool
|
||||||
|
"""
|
||||||
|
|
||||||
|
if name not in self._tools:
|
||||||
|
raise KeyError('%s not in Tools' % name)
|
||||||
|
|
||||||
|
self._remove_keys(name)
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
for k in validate_stringlist(key):
|
||||||
|
if k in self._keys:
|
||||||
|
warnings.warn('Key %s changed from %s to %s' %
|
||||||
|
(k, self._keys[k], name))
|
||||||
|
self._keys[k] = name
|
||||||
|
|
||||||
|
def remove_tool(self, name):
|
||||||
|
"""
|
||||||
|
Remove tool from `ToolManager`
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : string
|
||||||
|
Name of the Tool
|
||||||
|
"""
|
||||||
|
|
||||||
|
tool = self.get_tool(name)
|
||||||
|
tool.destroy()
|
||||||
|
|
||||||
|
# If is a toggle tool and toggled, untoggle
|
||||||
|
if getattr(tool, 'toggled', False):
|
||||||
|
self.trigger_tool(tool, 'toolmanager')
|
||||||
|
|
||||||
|
self._remove_keys(name)
|
||||||
|
|
||||||
|
s = 'tool_removed_event'
|
||||||
|
event = ToolEvent(s, self, tool)
|
||||||
|
self._callbacks.process(s, event)
|
||||||
|
|
||||||
|
del self._tools[name]
|
||||||
|
|
||||||
|
def add_tool(self, name, tool, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add *tool* to `ToolManager`
|
||||||
|
|
||||||
|
If successful adds a new event `tool_trigger_name` where **name** is
|
||||||
|
the **name** of the tool, this event is fired everytime
|
||||||
|
the tool is triggered.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
Name of the tool, treated as the ID, has to be unique
|
||||||
|
tool : class_like, i.e. str or type
|
||||||
|
Reference to find the class of the Tool to added.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
args and kwargs get passed directly to the tools constructor.
|
||||||
|
|
||||||
|
See Also
|
||||||
|
--------
|
||||||
|
matplotlib.backend_tools.ToolBase : The base class for tools.
|
||||||
|
"""
|
||||||
|
|
||||||
|
tool_cls = self._get_cls_to_instantiate(tool)
|
||||||
|
if not tool_cls:
|
||||||
|
raise ValueError('Impossible to find class for %s' % str(tool))
|
||||||
|
|
||||||
|
if name in self._tools:
|
||||||
|
warnings.warn('A "Tool class" with the same name already exists, '
|
||||||
|
'not added')
|
||||||
|
return self._tools[name]
|
||||||
|
|
||||||
|
tool_obj = tool_cls(self, name, *args, **kwargs)
|
||||||
|
self._tools[name] = tool_obj
|
||||||
|
|
||||||
|
if tool_cls.default_keymap is not None:
|
||||||
|
self.update_keymap(name, tool_cls.default_keymap)
|
||||||
|
|
||||||
|
# For toggle tools init the radio_group in self._toggled
|
||||||
|
if isinstance(tool_obj, tools.ToolToggleBase):
|
||||||
|
# None group is not mutually exclusive, a set is used to keep track
|
||||||
|
# of all toggled tools in this group
|
||||||
|
if tool_obj.radio_group is None:
|
||||||
|
self._toggled.setdefault(None, set())
|
||||||
|
else:
|
||||||
|
self._toggled.setdefault(tool_obj.radio_group, None)
|
||||||
|
|
||||||
|
# If initially toggled
|
||||||
|
if tool_obj.toggled:
|
||||||
|
self._handle_toggle(tool_obj, None, None, None)
|
||||||
|
tool_obj.set_figure(self.figure)
|
||||||
|
|
||||||
|
self._tool_added_event(tool_obj)
|
||||||
|
return tool_obj
|
||||||
|
|
||||||
|
def _tool_added_event(self, tool):
|
||||||
|
s = 'tool_added_event'
|
||||||
|
event = ToolEvent(s, self, tool)
|
||||||
|
self._callbacks.process(s, event)
|
||||||
|
|
||||||
|
def _handle_toggle(self, tool, sender, canvasevent, data):
|
||||||
|
"""
|
||||||
|
Toggle tools, need to untoggle prior to using other Toggle tool
|
||||||
|
Called from trigger_tool
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
tool: Tool object
|
||||||
|
sender: object
|
||||||
|
Object that wishes to trigger the tool
|
||||||
|
canvasevent : Event
|
||||||
|
Original Canvas event or None
|
||||||
|
data : Object
|
||||||
|
Extra data to pass to the tool when triggering
|
||||||
|
"""
|
||||||
|
|
||||||
|
radio_group = tool.radio_group
|
||||||
|
# radio_group None is not mutually exclusive
|
||||||
|
# just keep track of toggled tools in this group
|
||||||
|
if radio_group is None:
|
||||||
|
if tool.name in self._toggled[None]:
|
||||||
|
self._toggled[None].remove(tool.name)
|
||||||
|
else:
|
||||||
|
self._toggled[None].add(tool.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If the tool already has a toggled state, untoggle it
|
||||||
|
if self._toggled[radio_group] == tool.name:
|
||||||
|
toggled = None
|
||||||
|
# If no tool was toggled in the radio_group
|
||||||
|
# toggle it
|
||||||
|
elif self._toggled[radio_group] is None:
|
||||||
|
toggled = tool.name
|
||||||
|
# Other tool in the radio_group is toggled
|
||||||
|
else:
|
||||||
|
# Untoggle previously toggled tool
|
||||||
|
self.trigger_tool(self._toggled[radio_group],
|
||||||
|
self,
|
||||||
|
canvasevent,
|
||||||
|
data)
|
||||||
|
toggled = tool.name
|
||||||
|
|
||||||
|
# Keep track of the toggled tool in the radio_group
|
||||||
|
self._toggled[radio_group] = toggled
|
||||||
|
|
||||||
|
def _get_cls_to_instantiate(self, callback_class):
|
||||||
|
# Find the class that corresponds to the tool
|
||||||
|
if isinstance(callback_class, str):
|
||||||
|
# FIXME: make more complete searching structure
|
||||||
|
if callback_class in globals():
|
||||||
|
callback_class = globals()[callback_class]
|
||||||
|
else:
|
||||||
|
mod = 'backend_tools'
|
||||||
|
current_module = __import__(mod,
|
||||||
|
globals(), locals(), [mod], 1)
|
||||||
|
|
||||||
|
callback_class = getattr(current_module, callback_class, False)
|
||||||
|
if callable(callback_class):
|
||||||
|
return callback_class
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def trigger_tool(self, name, sender=None, canvasevent=None,
|
||||||
|
data=None):
|
||||||
|
"""
|
||||||
|
Trigger a tool and emit the tool_trigger_[name] event
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : string
|
||||||
|
Name of the tool
|
||||||
|
sender: object
|
||||||
|
Object that wishes to trigger the tool
|
||||||
|
canvasevent : Event
|
||||||
|
Original Canvas event or None
|
||||||
|
data : Object
|
||||||
|
Extra data to pass to the tool when triggering
|
||||||
|
"""
|
||||||
|
tool = self.get_tool(name)
|
||||||
|
if tool is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if sender is None:
|
||||||
|
sender = self
|
||||||
|
|
||||||
|
self._trigger_tool(name, sender, canvasevent, data)
|
||||||
|
|
||||||
|
s = 'tool_trigger_%s' % name
|
||||||
|
event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
|
||||||
|
self._callbacks.process(s, event)
|
||||||
|
|
||||||
|
def _trigger_tool(self, name, sender=None, canvasevent=None, data=None):
|
||||||
|
"""
|
||||||
|
Trigger on a tool
|
||||||
|
|
||||||
|
Method to actually trigger the tool
|
||||||
|
"""
|
||||||
|
tool = self.get_tool(name)
|
||||||
|
|
||||||
|
if isinstance(tool, tools.ToolToggleBase):
|
||||||
|
self._handle_toggle(tool, sender, canvasevent, data)
|
||||||
|
|
||||||
|
# Important!!!
|
||||||
|
# This is where the Tool object gets triggered
|
||||||
|
tool.trigger(sender, canvasevent, data)
|
||||||
|
|
||||||
|
def _key_press(self, event):
|
||||||
|
if event.key is None or self.keypresslock.locked():
|
||||||
|
return
|
||||||
|
|
||||||
|
name = self._keys.get(event.key, None)
|
||||||
|
if name is None:
|
||||||
|
return
|
||||||
|
self.trigger_tool(name, canvasevent=event)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tools(self):
|
||||||
|
"""Return the tools controlled by `ToolManager`"""
|
||||||
|
|
||||||
|
return self._tools
|
||||||
|
|
||||||
|
def get_tool(self, name, warn=True):
|
||||||
|
"""
|
||||||
|
Return the tool object, also accepts the actual tool for convenience
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str, ToolBase
|
||||||
|
Name of the tool, or the tool itself
|
||||||
|
warn : bool, optional
|
||||||
|
If this method should give warnings.
|
||||||
|
"""
|
||||||
|
if isinstance(name, tools.ToolBase) and name.name in self._tools:
|
||||||
|
return name
|
||||||
|
if name not in self._tools:
|
||||||
|
if warn:
|
||||||
|
warnings.warn("ToolManager does not control tool %s" % name)
|
||||||
|
return None
|
||||||
|
return self._tools[name]
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import importlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import matplotlib
|
||||||
|
from matplotlib import cbook
|
||||||
|
from matplotlib.backend_bases import _Backend
|
||||||
|
|
||||||
|
_log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
|
||||||
|
# attribute here for backcompat.
|
||||||
|
|
||||||
|
|
||||||
|
def _get_running_interactive_framework():
|
||||||
|
"""
|
||||||
|
Return the interactive framework whose event loop is currently running, if
|
||||||
|
any, or "headless" if no event loop can be started, or None.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Optional[str]
|
||||||
|
One of the following values: "qt5", "qt4", "gtk3", "wx", "tk",
|
||||||
|
"macosx", "headless", ``None``.
|
||||||
|
"""
|
||||||
|
QtWidgets = (sys.modules.get("PyQt5.QtWidgets")
|
||||||
|
or sys.modules.get("PySide2.QtWidgets"))
|
||||||
|
if QtWidgets and QtWidgets.QApplication.instance():
|
||||||
|
return "qt5"
|
||||||
|
QtGui = (sys.modules.get("PyQt4.QtGui")
|
||||||
|
or sys.modules.get("PySide.QtGui"))
|
||||||
|
if QtGui and QtGui.QApplication.instance():
|
||||||
|
return "qt4"
|
||||||
|
Gtk = (sys.modules.get("gi.repository.Gtk")
|
||||||
|
or sys.modules.get("pgi.repository.Gtk"))
|
||||||
|
if Gtk and Gtk.main_level():
|
||||||
|
return "gtk3"
|
||||||
|
wx = sys.modules.get("wx")
|
||||||
|
if wx and wx.GetApp():
|
||||||
|
return "wx"
|
||||||
|
tkinter = sys.modules.get("tkinter")
|
||||||
|
if tkinter:
|
||||||
|
for frame in sys._current_frames().values():
|
||||||
|
while frame:
|
||||||
|
if frame.f_code == tkinter.mainloop.__code__:
|
||||||
|
return "tk"
|
||||||
|
frame = frame.f_back
|
||||||
|
if 'matplotlib.backends._macosx' in sys.modules:
|
||||||
|
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
|
||||||
|
return "macosx"
|
||||||
|
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
|
||||||
|
return "headless"
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@cbook.deprecated("3.0")
|
||||||
|
def pylab_setup(name=None):
|
||||||
|
"""
|
||||||
|
Return new_figure_manager, draw_if_interactive and show for pyplot.
|
||||||
|
|
||||||
|
This provides the backend-specific functions that are used by pyplot to
|
||||||
|
abstract away the difference between backends.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
name : str, optional
|
||||||
|
The name of the backend to use. If `None`, falls back to
|
||||||
|
``matplotlib.get_backend()`` (which return :rc:`backend`).
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
backend_mod : module
|
||||||
|
The module which contains the backend of choice
|
||||||
|
|
||||||
|
new_figure_manager : function
|
||||||
|
Create a new figure manager (roughly maps to GUI window)
|
||||||
|
|
||||||
|
draw_if_interactive : function
|
||||||
|
Redraw the current figure if pyplot is interactive
|
||||||
|
|
||||||
|
show : function
|
||||||
|
Show (and possibly block) any unshown figures.
|
||||||
|
"""
|
||||||
|
# Import the requested backend into a generic module object.
|
||||||
|
if name is None:
|
||||||
|
name = matplotlib.get_backend()
|
||||||
|
backend_name = (name[9:] if name.startswith("module://")
|
||||||
|
else "matplotlib.backends.backend_{}".format(name.lower()))
|
||||||
|
backend_mod = importlib.import_module(backend_name)
|
||||||
|
# Create a local Backend class whose body corresponds to the contents of
|
||||||
|
# the backend module. This allows the Backend class to fill in the missing
|
||||||
|
# methods through inheritance.
|
||||||
|
Backend = type("Backend", (_Backend,), vars(backend_mod))
|
||||||
|
|
||||||
|
# Need to keep a global reference to the backend for compatibility reasons.
|
||||||
|
# See https://github.com/matplotlib/matplotlib/issues/6092
|
||||||
|
global backend
|
||||||
|
backend = name
|
||||||
|
|
||||||
|
_log.debug('backend %s version %s', name, Backend.backend_version)
|
||||||
|
return (backend_mod,
|
||||||
|
Backend.new_figure_manager,
|
||||||
|
Backend.draw_if_interactive,
|
||||||
|
Backend.show)
|
||||||