Compare commits
612 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c308339573 | ||
|
|
bd6a439e0b | ||
|
|
c90dd4902f | ||
|
|
868e5c45fe | ||
|
|
7e1852ee88 | ||
|
|
5ae98bc7c9 | ||
|
|
1624e7de7d | ||
|
|
7a98f84890 | ||
|
|
9b181280d4 | ||
|
|
38e94b9e43 | ||
|
|
dc25bc6f4d | ||
|
|
f64a205603 | ||
|
|
a8480f5492 | ||
|
|
0acf26b37c | ||
|
|
2c068a1a15 | ||
|
|
b961c95121 | ||
|
|
82693211f0 | ||
|
|
e5467462cb | ||
|
|
ab5a4102cd | ||
|
|
d5ef937b31 | ||
|
|
bce02e3f51 | ||
|
|
aad9744d85 | ||
|
|
506b781f3a | ||
|
|
267fd5b9bc | ||
|
|
c362f2869e | ||
|
|
4f7f437527 | ||
|
|
ab5da5ba28 | ||
|
|
7531bb8942 | ||
|
|
811ee8ccdc | ||
|
|
51cb485ce3 | ||
|
|
83d08b8b8f | ||
|
|
ed3d44d591 | ||
|
|
895fe23203 | ||
|
|
af04a560c8 | ||
|
|
9124a17e86 | ||
|
|
99b0d4c111 | ||
|
|
84b98c234f | ||
|
|
bcc1428ebf | ||
|
|
2133056c04 | ||
|
|
4155a9d7f9 | ||
|
|
ed4825523c | ||
|
|
fdbb2bbdab | ||
|
|
c064d6d847 | ||
|
|
d33874bd3d | ||
|
|
96d32dd11f | ||
|
|
d6bc354ff3 | ||
|
|
7325a4fb4b | ||
|
|
90f00c5b29 | ||
|
|
8539c423ea | ||
|
|
e9bfbfce84 | ||
|
|
6e4f0664cb | ||
|
|
3c920c9d94 | ||
|
|
15b67b2c6c | ||
|
|
973537fd9a | ||
|
|
d70a74479d | ||
|
|
946e4b750a | ||
|
|
9789e3fb9b | ||
|
|
6a15679d87 | ||
|
|
ad1cd5577c | ||
|
|
55668ca621 | ||
|
|
0c221a28d1 | ||
|
|
a475551b23 | ||
|
|
bad7676414 | ||
|
|
51c0470f0b | ||
|
|
4530047c76 | ||
|
|
1bf83c3bf7 | ||
|
|
bb6ab11001 | ||
|
|
e4bd1884d3 | ||
|
|
4014a48f7d | ||
|
|
97ded32415 | ||
|
|
593ac081f0 | ||
|
|
69f0a4e1cb | ||
|
|
757e7de60c | ||
|
|
1d7d5469a9 | ||
|
|
98afc3e590 | ||
|
|
ea189790f1 | ||
|
|
62987077fa | ||
|
|
3b9f7cb3f1 | ||
|
|
5882c7e344 | ||
|
|
77b6e2cd2e | ||
|
|
88b1f956c7 | ||
|
|
d9b49ca2bc | ||
|
|
4156ad5a30 | ||
|
|
ae46561648 | ||
|
|
1700a807e9 | ||
|
|
0bab15b213 | ||
|
|
38d3075554 | ||
|
|
1b124bfb87 | ||
|
|
cdf4dd0302 | ||
|
|
a13bed2db6 | ||
|
|
26318f94fe | ||
|
|
769fb0820f | ||
|
|
52380534f3 | ||
|
|
2fd2526046 | ||
|
|
61509bbd44 | ||
|
|
76499afd8d | ||
|
|
4023f328f7 | ||
|
|
4f49cb555b | ||
|
|
4dc959a3e4 | ||
|
|
518fbf562c | ||
|
|
49828d3d9d | ||
|
|
248e6a7b05 | ||
|
|
5561e6b770 | ||
|
|
ab083b86f3 | ||
|
|
4bf525222a | ||
|
|
45efcb381c | ||
|
|
07a7736c71 | ||
|
|
d2826ab7af | ||
|
|
6ab769f382 | ||
|
|
3e1cd2bdca | ||
|
|
22784b7f06 | ||
|
|
c4922615eb | ||
|
|
7307ddad3c | ||
|
|
c7ba143d03 | ||
|
|
b24206387b | ||
|
|
64d196442f | ||
|
|
5944643da6 | ||
|
|
8c5e495272 | ||
|
|
bb23685b9d | ||
|
|
940659bc14 | ||
|
|
7c5933732b | ||
|
|
89df2fcf76 | ||
|
|
174a199c30 | ||
|
|
6f1e7c3016 | ||
|
|
9f81e23f8f | ||
|
|
19fab6eea7 | ||
|
|
63161b11c3 | ||
|
|
4f886d65ec | ||
|
|
62e0e195e8 | ||
|
|
e9d4749f44 | ||
|
|
93f8e7d8e9 | ||
|
|
3dea6e0da5 | ||
|
|
6fb3b305ad | ||
|
|
7dfe891cc1 | ||
|
|
5c3966a32d | ||
|
|
c198535292 | ||
|
|
ece69fbb75 | ||
|
|
458651fa3e | ||
|
|
b2f7081c6f | ||
|
|
c255e89219 | ||
|
|
2edce5bf8a | ||
|
|
8f299448ea | ||
|
|
ecb951bb74 | ||
|
|
0dff6a9030 | ||
|
|
2095095895 | ||
|
|
4fc8a17f2a | ||
|
|
3cb737f8c8 | ||
|
|
7449aa813b | ||
|
|
7a3bcc227c | ||
|
|
b669a14081 | ||
|
|
9db4cc8c6d | ||
|
|
65c744f242 | ||
|
|
82bcadf7f8 | ||
|
|
f0f8478922 | ||
|
|
40e7057bce | ||
|
|
11a8e3591d | ||
|
|
5bea3986b2 | ||
|
|
89cb726284 | ||
|
|
4e9392d21b | ||
|
|
b785d27241 | ||
|
|
451860afca | ||
|
|
4cf4e3805c | ||
|
|
ef2670ca32 | ||
|
|
8626c80d3a | ||
|
|
5cb3cc2ed8 | ||
|
|
73cd2ea3b1 | ||
|
|
cd2ab42abc | ||
|
|
bf1f5ea3de | ||
|
|
79460abdd2 | ||
|
|
1e6e144d24 | ||
|
|
fe66557bbb | ||
|
|
f8e2ce6ff3 | ||
|
|
19677438c2 | ||
|
|
9012b11101 | ||
|
|
f70d301f0d | ||
|
|
b7d1f78f5e | ||
|
|
3384e7598e | ||
|
|
382b8e85da | ||
|
|
0a3121c243 | ||
|
|
ecfe1a2417 | ||
|
|
609ae22bda | ||
|
|
94578cb6a7 | ||
|
|
8a5f050f6c | ||
|
|
5c5b393f2f | ||
|
|
f837733d85 | ||
|
|
66b498de25 | ||
|
|
659bf6d7e8 | ||
|
|
a8a16900e7 | ||
|
|
e50b59e553 | ||
|
|
231804e2b4 | ||
|
|
421a86ade5 | ||
|
|
140a055e4e | ||
|
|
5bf86d91ec | ||
|
|
715cdd98fb | ||
|
|
7a01d9dbcb | ||
|
|
58dfa436ee | ||
|
|
8ab5e5015a | ||
|
|
a92190c3ae | ||
|
|
055fb6110f | ||
|
|
19ab3e2fbd | ||
|
|
ae54ac9d58 | ||
|
|
e6e902e8df | ||
|
|
d8638dbcf3 | ||
|
|
9795e4a532 | ||
|
|
e11ef994bb | ||
|
|
472679bb38 | ||
|
|
d066f8b726 | ||
|
|
aa5bbe6149 | ||
|
|
9c83b900ae | ||
|
|
f0289cef3a | ||
|
|
96844c5c12 | ||
|
|
1a29163530 | ||
|
|
b67906cfe1 | ||
|
|
153667505f | ||
|
|
b6f4062874 | ||
|
|
44be42c922 | ||
|
|
5f6ef84c4e | ||
|
|
551c97112c | ||
|
|
d5c5c0d6ac | ||
|
|
98902599ff | ||
|
|
7f3c205c46 | ||
|
|
199342a2d3 | ||
|
|
d397c0bbf8 | ||
|
|
6e5a93a6e1 | ||
|
|
df89bec712 | ||
|
|
fc921d63a1 | ||
|
|
aed473d0d2 | ||
|
|
7115c5458d | ||
|
|
8cb0bea57c | ||
|
|
1fa18922fa | ||
|
|
49cefc8b00 | ||
|
|
cbc70d3738 | ||
|
|
b855e2f1a6 | ||
|
|
bc7d0ffad2 | ||
|
|
2f2ed229fb | ||
|
|
b5875764ed | ||
|
|
f1e80a8e1b | ||
|
|
5a0e06e7a2 | ||
|
|
ed2feaf84b | ||
|
|
a1338ed52e | ||
|
|
d41463ba72 | ||
|
|
0e7af8d8a6 | ||
|
|
32980a653c | ||
|
|
d15a212e64 | ||
|
|
1ce8acc154 | ||
|
|
e8088d5c9a | ||
|
|
8081080709 | ||
|
|
8c619e8383 | ||
|
|
1c544c9845 | ||
|
|
ca66ba4893 | ||
|
|
afa1f0916b | ||
|
|
69d37d8b2a | ||
|
|
b62d7e3e8e | ||
|
|
e8f1baba43 | ||
|
|
ffe6fb849f | ||
|
|
3602d940eb | ||
|
|
9389f89889 | ||
|
|
edcc2a11c6 | ||
|
|
2adc8a0274 | ||
|
|
2432c3155a | ||
|
|
2132f6715e | ||
|
|
e895807158 | ||
|
|
c87f34285a | ||
|
|
17a317554c | ||
|
|
a871ee7858 | ||
|
|
2e6b4b121f | ||
|
|
df533d30fc | ||
|
|
1f761c61dd | ||
|
|
9bf5620887 | ||
|
|
1971bf653c | ||
|
|
c20ee0c129 | ||
|
|
6aadc6ec13 | ||
|
|
8eb4cbf66e | ||
|
|
0e0eaa0ccd | ||
|
|
d454fb402b | ||
|
|
9ae231aeb8 | ||
|
|
e783219a76 | ||
|
|
49e4f70f78 | ||
|
|
62c71110df | ||
|
|
544b3f350f | ||
|
|
9914901099 | ||
|
|
badac7379e | ||
|
|
7dd01cff6f | ||
|
|
4b77710016 | ||
|
|
1667d15f31 | ||
|
|
bae21194c1 | ||
|
|
4f125eedb5 | ||
|
|
f300ee1010 | ||
|
|
85d03f5e18 | ||
|
|
3f889492f9 | ||
|
|
508feb2bad | ||
|
|
9f8eef498c | ||
|
|
561828c2f8 | ||
|
|
fc1e63761c | ||
|
|
38b8db8051 | ||
|
|
f17cfcbfa2 | ||
|
|
7eceb140be | ||
|
|
b93f9ec598 | ||
|
|
36a6714e9e | ||
|
|
30071eef09 | ||
|
|
e85978a7ed | ||
|
|
d1f43e3e28 | ||
|
|
de39923d06 | ||
|
|
f800ff16c1 | ||
|
|
ef118dee6a | ||
|
|
2bc1674237 | ||
|
|
9445549aff | ||
|
|
95eb928beb | ||
|
|
a8fd23dfa4 | ||
|
|
7f2805a3c5 | ||
|
|
a8715294b0 | ||
|
|
c7467f544c | ||
|
|
30ef8eba37 | ||
|
|
23a5463208 | ||
|
|
84213ab8cd | ||
|
|
379cf6cbd9 | ||
|
|
a029eaa451 | ||
|
|
ac512ef731 | ||
|
|
80974958bd | ||
|
|
cc058e5c9e | ||
|
|
061d4f1ac7 | ||
|
|
88f4260777 | ||
|
|
66c2b84cb4 | ||
|
|
348c09624f | ||
|
|
3e551e40a7 | ||
|
|
c474581138 | ||
|
|
5646aa03d2 | ||
|
|
6b5d6b4156 | ||
|
|
4e1af9a2ca | ||
|
|
7d1bec11f9 | ||
|
|
c60476eadd | ||
|
|
d79220fb71 | ||
|
|
a8131dda7a | ||
|
|
10011d572a | ||
|
|
d7d10d2847 | ||
|
|
aa01283774 | ||
|
|
1b9458dcf0 | ||
|
|
e243a14c64 | ||
|
|
198226a39f | ||
|
|
2bfb9cc7dd | ||
|
|
71bdabe1a1 | ||
|
|
9b3b1f69df | ||
|
|
3f21a898c9 | ||
|
|
bcbe08bd5c | ||
|
|
4247cabb17 | ||
|
|
9a2c7740f0 | ||
|
|
efdfa81f21 | ||
|
|
15ee6d82e3 | ||
|
|
d15c557cd6 | ||
|
|
08d682501e | ||
|
|
baf22c3c60 | ||
|
|
5a937f1d0b | ||
|
|
d6bb8c6935 | ||
|
|
b48101ca51 | ||
|
|
572c6ee85e | ||
|
|
b0a1f07818 | ||
|
|
cb23362a5b | ||
|
|
7c810dbd20 | ||
|
|
82a8e6316f | ||
|
|
aaef338539 | ||
|
|
e7ce38b486 | ||
|
|
862042bb49 | ||
|
|
cbca740f9f | ||
|
|
e36bc2477a | ||
|
|
55afda3a7e | ||
|
|
ee9141a31a | ||
|
|
299e31fdff | ||
|
|
f9427b9bfb | ||
|
|
88726773f1 | ||
|
|
29e3dd404d | ||
|
|
1101de5ae4 | ||
|
|
f43e5f18d5 | ||
|
|
2aea1943d6 | ||
|
|
7dd5b6716e | ||
|
|
2425b8f614 | ||
|
|
d11dd83c94 | ||
|
|
6731a287b5 | ||
|
|
cf785b4d03 | ||
|
|
8b694ddd7a | ||
|
|
187149caeb | ||
|
|
19f5371787 | ||
|
|
9cdd289081 | ||
|
|
e813726afb | ||
|
|
a704d4ddee | ||
|
|
fa0497de5e | ||
|
|
7bba9f73d0 | ||
|
|
83d5ad216a | ||
|
|
7306b9caaa | ||
|
|
2b237f483f | ||
|
|
24e85f52b4 | ||
|
|
1fed498e33 | ||
|
|
f034f2701e | ||
|
|
8312f1c2aa | ||
|
|
3522bf9b09 | ||
|
|
8a69235220 | ||
|
|
b295436bff | ||
|
|
8cfe9e96b8 | ||
|
|
65efe53bfc | ||
|
|
09c95fb28a | ||
|
|
212284cbd4 | ||
|
|
082645d312 | ||
|
|
18b8e6fa58 | ||
|
|
9d7c05a015 | ||
|
|
3c92ba9bd5 | ||
|
|
00970d6b99 | ||
|
|
979782d1cf | ||
|
|
04f3e3bc8f | ||
|
|
afff78868f | ||
|
|
8020464602 | ||
|
|
8135c240dc | ||
|
|
19983147a3 | ||
|
|
d5b22dd86e | ||
|
|
917bc984eb | ||
|
|
1a952a4961 | ||
|
|
ee00aa6522 | ||
|
|
2e0e6e3342 | ||
|
|
d4641e4457 | ||
|
|
8b95d17b7b | ||
|
|
f407bd45fd | ||
|
|
fa65888590 | ||
|
|
dbb9356d7e | ||
|
|
1ac2fccd2a | ||
|
|
57bffe099f | ||
|
|
a016d181d7 | ||
|
|
759c8d5377 | ||
|
|
bd68378d9c | ||
|
|
7a7f61a296 | ||
|
|
3e742c74bb | ||
|
|
6a34b729e9 | ||
|
|
4191b75966 | ||
|
|
d5b8414fde | ||
|
|
57ebb2bacf | ||
|
|
914f23432f | ||
|
|
967b2ffeb0 | ||
|
|
131372740e | ||
|
|
51024b5f8a | ||
|
|
141dc12e44 | ||
|
|
e230d9db4a | ||
|
|
4a8e62b439 | ||
|
|
f949b88ebd | ||
|
|
ab71ee4f93 | ||
|
|
8ebe074954 | ||
|
|
f25e37579d | ||
|
|
3ef79bbaf3 | ||
|
|
73601e4acb | ||
|
|
a9fd2271dd | ||
|
|
7c2d485de0 | ||
|
|
30da408ace | ||
|
|
485234bc78 | ||
|
|
13095eb550 | ||
|
|
40e1787948 | ||
|
|
7e8053abef | ||
|
|
ff44e46d7b | ||
|
|
d71a67cdad | ||
|
|
54e4be1d13 | ||
|
|
fb0750710e | ||
|
|
5618df78f8 | ||
|
|
668ede1133 | ||
|
|
deb197a1d8 | ||
|
|
55c549d86f | ||
|
|
78eb1e6a54 | ||
|
|
4c8523efc0 | ||
|
|
4400c70514 | ||
|
|
3105c9f9ae | ||
|
|
04388a7b9b | ||
|
|
db30f20341 | ||
|
|
65d9feb0a9 | ||
|
|
77c5c9400a | ||
|
|
5de8740a38 | ||
|
|
ea8a7ec1ba | ||
|
|
b38590a0bb | ||
|
|
ee2d3330aa | ||
|
|
aa2b8b42d0 | ||
|
|
91e272546a | ||
|
|
d90a2c861a | ||
|
|
a117498991 | ||
|
|
e29d71d0ff | ||
|
|
bacbd909d1 | ||
|
|
77d8ccba87 | ||
|
|
347c39b8e9 | ||
|
|
bc58c5d031 | ||
|
|
267c0191a8 | ||
|
|
9ed8c32f76 | ||
|
|
1a4c7afbef | ||
|
|
c58fd63efc | ||
|
|
fa13b77cfa | ||
|
|
c490138afe | ||
|
|
327fceefb7 | ||
|
|
1631137da1 | ||
|
|
4661b0210d | ||
|
|
a8136bb9f5 | ||
|
|
4c6026fdda | ||
|
|
55baab413a | ||
|
|
2f60801059 | ||
|
|
2644dd1984 | ||
|
|
c35e0e831c | ||
|
|
60ed3e4050 | ||
|
|
dd662eaca3 | ||
|
|
e3da785321 | ||
|
|
f4c92da4e6 | ||
|
|
eb208b921c | ||
|
|
8a579e2a2a | ||
|
|
cdb573e793 | ||
|
|
ad5507c5f4 | ||
|
|
24a68f5c48 | ||
|
|
0d800fa302 | ||
|
|
dc33506bfa | ||
|
|
89f9efd3a3 | ||
|
|
52c2b2348a | ||
|
|
b5fe9dbe33 | ||
|
|
5bc50210ad | ||
|
|
fe402465b1 | ||
|
|
3ee9a68c09 | ||
|
|
29170512ab | ||
|
|
b5614a433e | ||
|
|
5f005d67e3 | ||
|
|
c78d3682ac | ||
|
|
fe6628b0a4 | ||
|
|
cb384d051b | ||
|
|
849063c797 | ||
|
|
087c47a37e | ||
|
|
b193539fa4 | ||
|
|
ae7fb012a9 | ||
|
|
409233d4fc | ||
|
|
7d27692828 | ||
|
|
dee4c34411 | ||
|
|
eb3bfb5c56 | ||
|
|
57990bfd83 | ||
|
|
29f5ace436 | ||
|
|
0fcab30652 | ||
|
|
c53874c8ab | ||
|
|
e77d50bc61 | ||
|
|
f0c6a98027 | ||
|
|
58d78beeaa | ||
|
|
e1a3854f2a | ||
|
|
7630d73002 | ||
|
|
3cfc58e3a2 | ||
|
|
3ee102b79f | ||
|
|
f5180d8724 | ||
|
|
4738b01125 | ||
|
|
d1f0276408 | ||
|
|
1766f6edd6 | ||
|
|
8a406b18f5 | ||
|
|
2620aeca02 | ||
|
|
5c0e30ed70 | ||
|
|
607e1f941b | ||
|
|
d30567959b | ||
|
|
83e0e58888 | ||
|
|
5a870fe1c2 | ||
|
|
d846cb3d73 | ||
|
|
a0099ad6d6 | ||
|
|
f28c2b2557 | ||
|
|
52bbb38188 | ||
|
|
aa6e6bdf7a | ||
|
|
6c333d5010 | ||
|
|
673cce9e56 | ||
|
|
d79089c587 | ||
|
|
c4e3020369 | ||
|
|
77e9e6a5d7 | ||
|
|
a75d6bc52c | ||
|
|
c5fcbc0709 | ||
|
|
2f64c42062 | ||
|
|
d304ee005a | ||
|
|
82fa501dea | ||
|
|
bc7170769b | ||
|
|
6941f1de64 | ||
|
|
a3f549bb9e | ||
|
|
0bedaed367 | ||
|
|
1d0863abfe | ||
|
|
a3a9ec926f | ||
|
|
a8d0ae1361 | ||
|
|
9a08c45e59 | ||
|
|
88c9516e20 | ||
|
|
ec7dbed800 | ||
|
|
26d273643b | ||
|
|
0ca3f5ec80 | ||
|
|
d7ea47387f | ||
|
|
7b5b602e96 | ||
|
|
b78ec54650 | ||
|
|
77e4d4b2d4 | ||
|
|
2198fd4e3d | ||
|
|
bd47a29076 | ||
|
|
337c9d59ae | ||
|
|
54860a874c | ||
|
|
89d1a77281 | ||
|
|
b634d03ac3 | ||
|
|
ab64a26b5b | ||
|
|
a2f2ad84da | ||
|
|
c0fb9eeca4 | ||
|
|
42ac46c1e1 | ||
|
|
7b1075990c | ||
|
|
37be01bc12 | ||
|
|
7d90f26554 | ||
|
|
f1e10d8d25 | ||
|
|
b01d378475 | ||
|
|
a5f33fecd8 | ||
|
|
f4177bfa94 | ||
|
|
d8a2b621d8 | ||
|
|
9a33557112 | ||
|
|
efc6bf4eb8 | ||
|
|
17e711fda2 | ||
|
|
01a8c96820 | ||
|
|
d96b634ded | ||
|
|
afcdcf06a1 | ||
|
|
5597eb4cc4 | ||
|
|
3f0171b77b | ||
|
|
badcd8bfa1 | ||
|
|
04f1b176c4 | ||
|
|
899fe19afb | ||
|
|
f3168518e2 | ||
|
|
04d769ff24 | ||
|
|
01c2c7baf8 |
36
.codeclimate.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
engines:
|
||||
csslint:
|
||||
enabled: false
|
||||
duplication:
|
||||
enabled: false
|
||||
eslint:
|
||||
enabled: true
|
||||
config:
|
||||
config: superset/assets/.eslintrc
|
||||
pep8:
|
||||
enabled: true
|
||||
fixme:
|
||||
enabled: false
|
||||
radon:
|
||||
enabled: true
|
||||
checks:
|
||||
Complexity:
|
||||
enabled: false
|
||||
ratings:
|
||||
paths:
|
||||
- "**.py"
|
||||
- "superset/assets/**.js"
|
||||
- "superset/assets/**.jsx"
|
||||
exclude_paths:
|
||||
- ".*"
|
||||
- "**.pyc"
|
||||
- "**.gz"
|
||||
- "env/"
|
||||
- "tests/"
|
||||
- "superset/ascii_art.py"
|
||||
- "superset/assets/images/"
|
||||
- "superset/assets/vendor/"
|
||||
- "superset/assets/node_modules/"
|
||||
- "superset/assets/javascripts/dist/"
|
||||
- "superset/migrations"
|
||||
- "docs/"
|
||||
14
.gitignore
vendored
@@ -1,27 +1,33 @@
|
||||
*.pyc
|
||||
yarn-error.log
|
||||
_modules
|
||||
superset/assets/coverage/*
|
||||
changelog.sh
|
||||
babel
|
||||
.DS_Store
|
||||
.coverage
|
||||
_build
|
||||
_static
|
||||
_images
|
||||
caravel/bin/caravelc
|
||||
_modules
|
||||
superset/bin/supersetc
|
||||
env_py3
|
||||
.eggs
|
||||
build
|
||||
*.db
|
||||
tmp
|
||||
caravel_config.py
|
||||
superset_config.py
|
||||
local_config.py
|
||||
env
|
||||
dist
|
||||
caravel.egg-info/
|
||||
superset.egg-info/
|
||||
app.db
|
||||
*.bak
|
||||
.idea
|
||||
*.sqllite
|
||||
|
||||
# Node.js, webpack artifacts
|
||||
*.entry.js
|
||||
*.js.map
|
||||
node_modules
|
||||
npm-debug.log
|
||||
yarn.lock
|
||||
|
||||
@@ -16,8 +16,8 @@ pep8:
|
||||
full: true
|
||||
ignore-paths:
|
||||
- docs
|
||||
- caravel/migrations/env.py
|
||||
- caravel/ascii_art.py
|
||||
- superset/migrations/env.py
|
||||
- superset/ascii_art.py
|
||||
ignore-patterns:
|
||||
- ^example/doc_.*\.py$
|
||||
- (^|/)docs(/|$)
|
||||
|
||||
2
.pycodestyle
Normal file
@@ -0,0 +1,2 @@
|
||||
[pycodestyle]
|
||||
max-line-length = 90
|
||||
46
.travis.yml
@@ -1,22 +1,38 @@
|
||||
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
addons:
|
||||
code_climate:
|
||||
repo_token: 5f3a06c425eef7be4b43627d7d07a3e46c45bdc07155217825ff7c49cb6a470c
|
||||
apt:
|
||||
sources:
|
||||
- deadsnakes
|
||||
packages:
|
||||
- python3.5
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.wheelhouse/
|
||||
env:
|
||||
global:
|
||||
- TRAVIS_CACHE=$HOME/.travis_cache/
|
||||
- TRAVIS_NODE_VERSION="5.11"
|
||||
matrix:
|
||||
- TOX_ENV=javascript
|
||||
- TOX_ENV=py34-postgres
|
||||
- TOX_ENV=py34-sqlite
|
||||
- TOX_ENV=py27-mysql
|
||||
- TOX_ENV=py27-sqlite
|
||||
before_install:
|
||||
- npm install -g npm@'>=2.7.1'
|
||||
- npm install -g npm@'>=3.9.5'
|
||||
before_script:
|
||||
- mysql -e 'drop database if exists superset; create database superset DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci' -u root
|
||||
- mysql -u root -e "CREATE USER 'mysqluser'@'localhost' IDENTIFIED BY 'mysqluserpassword';"
|
||||
- mysql -u root -e "GRANT ALL ON superset.* TO 'mysqluser'@'localhost';"
|
||||
- psql -c 'create database superset;' -U postgres
|
||||
- psql -c "CREATE USER postgresuser WITH PASSWORD 'pguserpassword';" -U postgres
|
||||
- export PATH=${PATH}:/tmp/hive/bin
|
||||
install:
|
||||
- pip wheel -w $HOME/.wheelhouse -f $HOME/.wheelhouse .
|
||||
- pip install --find-links=$HOME/.wheelhouse --no-index .
|
||||
- pip install --find-links=$HOME/.wheelhouse --no-index -r dev-reqs.txt
|
||||
- cd caravel/assets
|
||||
- npm --version
|
||||
- pip install --upgrade pip
|
||||
- pip install tox tox-travis
|
||||
- rm -rf ~/.nvm && git clone https://github.com/creationix/nvm.git ~/.nvm && (cd ~/.nvm && git checkout `git describe --abbrev=0 --tags`) && source ~/.nvm/nvm.sh && nvm install $TRAVIS_NODE_VERSION
|
||||
- npm install
|
||||
- npm run lint
|
||||
- npm run prod
|
||||
- cd $TRAVIS_BUILD_DIR
|
||||
script: bash run_tests.sh
|
||||
after_success:
|
||||
- coveralls
|
||||
script: tox -e $TOX_ENV
|
||||
|
||||
1566
CHANGELOG.md
143
CONTRIBUTING.md
@@ -9,7 +9,7 @@ You can contribute in many ways:
|
||||
|
||||
### Report Bugs
|
||||
|
||||
Report bugs through Github
|
||||
Report bugs through GitHub
|
||||
|
||||
If you are reporting a bug, please include:
|
||||
|
||||
@@ -30,14 +30,14 @@ Look through the GitHub issues for features. Anything tagged with
|
||||
|
||||
### Documentation
|
||||
|
||||
Caravel could always use better documentation,
|
||||
whether as part of the official Caravel docs,
|
||||
Superset could always use better documentation,
|
||||
whether as part of the official Superset docs,
|
||||
in docstrings, `docs/*.rst` or even on the web as blog posts or
|
||||
articles.
|
||||
|
||||
### Submit Feedback
|
||||
|
||||
The best way to send feedback is to file an issue on Github.
|
||||
The best way to send feedback is to file an issue on GitHub.
|
||||
|
||||
If you are proposing a feature:
|
||||
|
||||
@@ -49,16 +49,16 @@ If you are proposing a feature:
|
||||
|
||||
## Latest Documentation
|
||||
|
||||
[API Documentation](http://pythonhosted.com/caravel)
|
||||
Latest documentation and tutorial are available [here](http://airbnb.io/superset)
|
||||
|
||||
## Setting up a Python development environment
|
||||
|
||||
Check the [OS dependencies](http://airbnb.io/caravel/installation.html#os-dependencies) before follows these steps.
|
||||
Check the [OS dependencies](http://airbnb.io/superset/installation.html#os-dependencies) before follows these steps.
|
||||
|
||||
# fork the repo on github and then clone it
|
||||
# fork the repo on GitHub and then clone it
|
||||
# alternatively you may want to clone the main repo but that won't work
|
||||
# so well if you are planning on sending PRs
|
||||
# git clone git@github.com:airbnb/caravel.git
|
||||
# git clone git@github.com:airbnb/superset.git
|
||||
|
||||
# [optional] setup a virtual env and activate it
|
||||
virtualenv env
|
||||
@@ -68,28 +68,30 @@ Check the [OS dependencies](http://airbnb.io/caravel/installation.html#os-depend
|
||||
python setup.py develop
|
||||
|
||||
# Create an admin user
|
||||
fabmanager create-admin --app caravel
|
||||
fabmanager create-admin --app superset
|
||||
|
||||
# Initialize the database
|
||||
caravel db upgrade
|
||||
superset db upgrade
|
||||
|
||||
# Create default roles and permissions
|
||||
caravel init
|
||||
superset init
|
||||
|
||||
# Load some data to play with
|
||||
caravel load_examples
|
||||
superset load_examples
|
||||
|
||||
# start a dev web server
|
||||
caravel runserver -d
|
||||
superset runserver -d
|
||||
|
||||
|
||||
## Setting up the node / npm javascript environment
|
||||
|
||||
`caravel/assets` contains all npm-managed, front end assets.
|
||||
`superset/assets` contains all npm-managed, front end assets.
|
||||
Flask-Appbuilder itself comes bundled with jQuery and bootstrap.
|
||||
While these may be phased out over time, these packages are currently not
|
||||
managed with npm.
|
||||
|
||||
### Node/npm versions
|
||||
Make sure you are using recent versions of node and npm. No problems have been found with node>=5.10 and npm>=3.9.
|
||||
|
||||
### Using npm to generate bundled files
|
||||
|
||||
@@ -102,23 +104,22 @@ echo prefix=~/.npm-packages >> ~/.npmrc
|
||||
curl -L https://www.npmjs.com/install.sh | sh
|
||||
```
|
||||
|
||||
The final step is to add
|
||||
`~/.node/bin` to your `PATH` so commands you install globally are usable.
|
||||
Add something like this to your `.bashrc` file.
|
||||
The final step is to add `~/.npm-packages/bin` to your `PATH` so commands you install globally are usable.
|
||||
Add something like this to your `.bashrc` file, then `source ~/.bashrc` to reflect the change.
|
||||
```
|
||||
export PATH="$HOME/.node/bin:$PATH"
|
||||
export PATH="$HOME/.npm-packages/bin:$PATH"
|
||||
```
|
||||
|
||||
#### npm packages
|
||||
To install third party libraries defined in `package.json`, run the
|
||||
following within the `caravel/assets/` directory which will install them in a
|
||||
following within the `superset/assets/` directory which will install them in a
|
||||
new `node_modules/` folder within `assets/`.
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
To parse and generate bundled files for caravel, run either of the
|
||||
To parse and generate bundled files for superset, run either of the
|
||||
following commands. The `dev` flag will keep the npm script running and
|
||||
re-run it upon any changes within the assets directory.
|
||||
|
||||
@@ -134,24 +135,57 @@ For every development session you will have to start a flask dev server
|
||||
as well as an npm watcher
|
||||
|
||||
```
|
||||
caravel runserver -d -p 8081
|
||||
superset runserver -d -p 8081
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Tests can then be run with:
|
||||
Python tests can be run with:
|
||||
|
||||
./run_unit_tests.sh
|
||||
./run_tests.sh
|
||||
|
||||
We use [Mocha](https://mochajs.org/), [Chai](http://chaijs.com/) and [Enzyme](http://airbnb.io/enzyme/) to test Javascript. Tests can be run with:
|
||||
|
||||
cd /superset/superset/assets/javascripts
|
||||
npm i
|
||||
npm run test
|
||||
|
||||
## Linting
|
||||
|
||||
Lint the project with:
|
||||
|
||||
# for python changes
|
||||
flake8 changes tests
|
||||
flake8 changes superset
|
||||
|
||||
# for javascript
|
||||
npm run lint
|
||||
|
||||
## Linting with codeclimate
|
||||
Codeclimate is a service we use to measure code quality and test coverage. To get codeclimate's report on your branch, ideally before sending your PR, you can setup codeclimate against your Superset fork. After you push to your fork, you should be able to get the report at http://codeclimate.com . Alternatively, if you prefer to work locally, you can install the codeclimate cli tool.
|
||||
|
||||
*Install the codeclimate cli tool*
|
||||
```
|
||||
curl -L https://github.com/docker/machine/releases/download/v0.7.0/docker-machine-`uname -s`-`uname -m` > /usr/local/bin/docker-machine && chmod +x /usr/local/bin/docker-machine
|
||||
brew install docker
|
||||
docker-machine create --driver virtual box default
|
||||
docker-machine env default
|
||||
eval "$(docker-machine env default)"
|
||||
docker pull codeclimate/codeclimate
|
||||
brew tap codeclimate/formulae
|
||||
brew install codeclimate
|
||||
```
|
||||
|
||||
*Run the lint command:*
|
||||
```
|
||||
docker-machine start
|
||||
eval "$(docker-machine env default)”
|
||||
codeclimate analyze
|
||||
```
|
||||
More info can be found here: https://docs.codeclimate.com/docs/open-source-free
|
||||
|
||||
|
||||
## API documentation
|
||||
|
||||
Generate the documentation with:
|
||||
@@ -159,12 +193,12 @@ Generate the documentation with:
|
||||
cd docs && ./build.sh
|
||||
|
||||
## CSS Themes
|
||||
As part of the npm build process, CSS for Caravel is compiled from ```Less```, a dynamic stylesheet language.
|
||||
As part of the npm build process, CSS for Superset is compiled from `Less`, a dynamic stylesheet language.
|
||||
|
||||
It's possible to customize or add your own theme to Caravel, either by overriding CSS rules or preferably
|
||||
by modifying the Less variables or files in ```assets/stylesheets/less/```.
|
||||
It's possible to customize or add your own theme to Superset, either by overriding CSS rules or preferably
|
||||
by modifying the Less variables or files in `assets/stylesheets/less/`.
|
||||
|
||||
The ```variables.less``` and ```bootswatch.less``` files that ship with Caravel are derived from
|
||||
The `variables.less` and `bootswatch.less` files that ship with Superset are derived from
|
||||
[Bootswatch](https://bootswatch.com) and thus extend Bootstrap. Modify variables in these files directly, or
|
||||
swap them out entirely with the equivalent files from other Bootswatch (themes)[https://github.com/thomaspark/bootswatch.git]
|
||||
|
||||
@@ -179,7 +213,60 @@ meets these guidelines:
|
||||
as part of the same PR. Doc string are often sufficient, make
|
||||
sure to follow the sphinx compatible standards.
|
||||
3. The pull request should work for Python 2.6, 2.7, and ideally python 3.3.
|
||||
`from __future__ import ` will be required in every `.py` file soon.
|
||||
``from __future__ import`` will be required in every `.py` file soon.
|
||||
4. Code will be reviewed by re running the unittests, flake8 and syntax
|
||||
should be as rigorous as the core Python project.
|
||||
5. Please rebase and resolve all conflicts before submitting.
|
||||
|
||||
|
||||
## Translations
|
||||
|
||||
We use [Babel](http://babel.pocoo.org/en/latest/) to translate Superset. The
|
||||
key is to instrument the strings that need translation using
|
||||
`from flask_babel import lazy_gettext as _`. Once this is imported in
|
||||
a module, all you have to do is to `_("Wrap your strings")` using the
|
||||
underscore `_` "function".
|
||||
|
||||
To enable changing language in your environment, you can simply add the
|
||||
`LANGUAGES` parameter to your `superset_config.py`. Having more than one
|
||||
options here will add a language selection dropdown on the right side of the
|
||||
navigation bar.
|
||||
|
||||
LANGUAGES = {
|
||||
'en': {'flag': 'us', 'name': 'English'},
|
||||
'fr': {'flag': 'fr', 'name': 'French'},
|
||||
'zh': {'flag': 'cn', 'name': 'Chinese'},
|
||||
}
|
||||
|
||||
As per the [Flask AppBuilder documentation] about translation, to create a
|
||||
new language dictionary, run the following command:
|
||||
|
||||
pybabel init -i ./babel/messages.pot -d superset/translations -l es
|
||||
|
||||
Then it's a matter of running the statement below to gather all stings that
|
||||
need translation
|
||||
|
||||
fabmanager babel-extract --target superset/translations/
|
||||
|
||||
You can then translate the strings gathered in files located under
|
||||
`superset/translation`, where there's one per language. For the translations
|
||||
to take effect, they need to be compiled using this command:
|
||||
|
||||
fabmanager babel-compile --target superset/translations/
|
||||
|
||||
|
||||
## Adding new datasources
|
||||
|
||||
1. Create Models and Views for the datasource, add them under superset folder, like a new my_models.py
|
||||
with models for cluster, datasources, columns and metrics and my_views.py with clustermodelview
|
||||
and datasourcemodelview.
|
||||
|
||||
2. Create db migration files for the new models
|
||||
|
||||
3. Specify this variable to add the datasource model and from which module it is from in config.py:
|
||||
|
||||
For example:
|
||||
|
||||
`ADDITIONAL_MODULE_DS_MAP = {'superset.my_models': ['MyDatasource', 'MyOtherDatasource']}`
|
||||
|
||||
This means it'll register MyDatasource and MyOtherDatasource in superset.my_models module in the source registry.
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
Please use [pull requests](https://github.com/airbnb/caravel/pull/new/master)
|
||||
Please use [pull requests](https://github.com/airbnb/superset/pull/new/master)
|
||||
to add your organization and/or project to this document!
|
||||
|
||||
Organizations
|
||||
----------
|
||||
- [Airbnb](https://github.com/airbnb)
|
||||
- [GfK Data Lab] (http://datalab.gfk.com)
|
||||
- [Maieutical Labs] (https://cloudschooling.it)
|
||||
- [Shopkick] (https://www.shopkick.com)
|
||||
- [Amino] (https://amino.com)
|
||||
- [Faasos] (http://faasos.com/)
|
||||
|
||||
Projects
|
||||
----------
|
||||
|
||||
19
ISSUE_TEMPLATE.md
Normal file
@@ -0,0 +1,19 @@
|
||||
Make sure these boxes are checked before submitting your issue - thank you!
|
||||
|
||||
- [ ] I have checked the superset logs for python stacktraces and included it here as text if any
|
||||
- [ ] I have reproduced the issue with at least the latest released version of superset
|
||||
- [ ] I have checked the issue tracker for the same issue and I haven't found one similar
|
||||
|
||||
|
||||
### Superset version
|
||||
|
||||
|
||||
### Expected results
|
||||
|
||||
|
||||
### Actual results
|
||||
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
|
||||
15
MANIFEST.in
@@ -1,8 +1,9 @@
|
||||
recursive-include caravel/templates *
|
||||
recursive-include caravel/static *
|
||||
recursive-exclude caravel/static/assets/node_modules *
|
||||
recursive-include caravel/static/assets/node_modules/font-awesome *
|
||||
recursive-exclude caravel/static/docs *
|
||||
recursive-include superset/templates *
|
||||
recursive-include superset/static *
|
||||
recursive-exclude superset/static/assets/node_modules *
|
||||
recursive-include superset/static/assets/node_modules/font-awesome *
|
||||
recursive-exclude superset/static/docs *
|
||||
recursive-exclude superset/static/spec *
|
||||
recursive-exclude tests *
|
||||
recursive-include caravel/data *
|
||||
recursive-include caravel/migrations *
|
||||
recursive-include superset/data *
|
||||
recursive-include superset/migrations *
|
||||
|
||||
97
README.md
@@ -1,36 +1,51 @@
|
||||
Caravel
|
||||
Superset
|
||||
=========
|
||||
<img src="http://i.imgur.com/H0Kyvyi.jpg" style="border-radius: 20px; box-shadow:5px 5px 5px gray;" alt="Caravel" width="500"/>
|
||||
|
||||
[](https://travis-ci.org/airbnb/caravel)
|
||||
[](https://badge.fury.io/py/caravel)
|
||||
[](https://coveralls.io/github/airbnb/caravel?branch=master)
|
||||
[](https://landscape.io/github/airbnb/caravel/master)
|
||||
[](https://requires.io/github/airbnb/caravel/requirements/?branch=master)
|
||||
[](https://gitter.im/airbnb/caravel?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://airbnb.io/caravel/)
|
||||
[](https://travis-ci.org/airbnb/superset)
|
||||
[](https://badge.fury.io/py/superset)
|
||||
[](https://coveralls.io/github/airbnb/superset?branch=master)
|
||||
[](https://codeclimate.com/github/airbnb/superset/coverage)
|
||||
[](https://landscape.io/github/airbnb/superset/master)
|
||||
[](https://codeclimate.com/github/airbnb/superset)
|
||||
[](https://pypi.python.org/pypi/superset)
|
||||
[](https://requires.io/github/airbnb/superset/requirements/?branch=master)
|
||||
[](https://gitter.im/airbnb/superset?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](http://airbnb.io/superset/)
|
||||
[](https://david-dm.org/airbnb/superset?path=superset/assets)
|
||||
|
||||
Caravel is a data exploration platform designed to be visual, intuitive
|
||||
**Superset** is a data exploration platform designed to be visual, intuitive
|
||||
and interactive.
|
||||
|
||||
[this project used to be named **Panoramix**]
|
||||
[this project used to be named **Caravel**, and **Panoramix** in the past]
|
||||
|
||||
|
||||
Video - Introduction to Caravel
|
||||
---------------------------------
|
||||
[](http://www.youtube.com/watch?v=3Txm_nj_R7M)
|
||||
Screenshots & Gifs
|
||||
------------------
|
||||
|
||||
Screenshots
|
||||
------------
|
||||

|
||||

|
||||
**View Dashboards**
|
||||

|
||||
|
||||
Caravel
|
||||
<br/>
|
||||
**View/Edit a Slice**
|
||||

|
||||
|
||||
<br/>
|
||||
**Query and Visualize with SQL Lab**
|
||||

|
||||
|
||||
<br/>
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Superset
|
||||
---------
|
||||
Caravel's main goal is to make it easy to slice, dice and visualize data.
|
||||
Superset's main goal is to make it easy to slice, dice and visualize data.
|
||||
It empowers users to perform **analytics at the speed of thought**.
|
||||
|
||||
Caravel provides:
|
||||
Superset provides:
|
||||
* A quick way to intuitively visualize datasets by allowing users to create
|
||||
and share interactive dashboards
|
||||
* A rich set of visualizations to analyze your data, as well as a flexible
|
||||
@@ -43,14 +58,15 @@ Caravel provides:
|
||||
displayed in the UI, by defining which fields should show up in
|
||||
which dropdown and which aggregation and function (metrics) are
|
||||
made available to the user
|
||||
* Deep integration with Druid allows for Caravel to stay blazing fast while
|
||||
* Deep integration with Druid allows for Superset to stay blazing fast while
|
||||
slicing and dicing large, realtime datasets
|
||||
* Fast loading dashboards with configurable caching
|
||||
|
||||
|
||||
Database Support
|
||||
----------------
|
||||
|
||||
Caravel was originally designed on top of Druid.io, but quickly broadened
|
||||
Superset was originally designed on top of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SQLAlchemy, a Python
|
||||
ORM that is compatible with
|
||||
[most common databases](http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html).
|
||||
@@ -71,41 +87,50 @@ power analytic dashboards and applications.*
|
||||
Installation & Configuration
|
||||
----------------------------
|
||||
|
||||
[See in the documentation](http://airbnb.io/caravel/installation.html)
|
||||
[See in the documentation](http://airbnb.io/superset/installation.html)
|
||||
|
||||
|
||||
More screenshots
|
||||
----------------
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
|
||||
Resources
|
||||
-------------
|
||||
* [Caravel Google Group](https://groups.google.com/forum/#!forum/airbnb_caravel)
|
||||
* [Gitter (live chat) Channel](https://gitter.im/airbnb/caravel)
|
||||
* [Docker image](https://hub.docker.com/r/kochalex/caravel/) (community contributed)
|
||||
* [Superset Google Group](https://groups.google.com/forum/#!forum/airbnb_superset)
|
||||
* [Gitter (live chat) Channel](https://gitter.im/airbnb/superset)
|
||||
* [Docker image](https://hub.docker.com/r/amancevice/superset/) (community contributed)
|
||||
* [Slides from Strata (March 2016)](https://drive.google.com/open?id=0B5PVE0gzO81oOVJkdF9aNkJMSmM)
|
||||
|
||||
|
||||
Tip of the Hat
|
||||
--------------
|
||||
|
||||
Caravel would not be possible without these great frameworks / libs
|
||||
Superset would not be possible without these great frameworks / libs
|
||||
|
||||
* Flask App Builder - Allowing us to focus on building the app quickly while
|
||||
getting the foundation for free
|
||||
* The Flask ecosystem - Simply amazing. So much Plug, easy play.
|
||||
* NVD3 - One of the best charting libraries out there
|
||||
* Much more, check out the `install_requires` section in the [setup.py](https://github.com/airbnb/caravel/blob/master/setup.py) file!
|
||||
* Much more, check out the `install_requires` section in the [setup.py](https://github.com/airbnb/superset/blob/master/setup.py) file!
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/airbnb/caravel/blob/master/CONTRIBUTING.md)
|
||||
Interested in contributing? Casual hacking? Check out [Contributing.MD](https://github.com/airbnb/superset/blob/master/CONTRIBUTING.md)
|
||||
|
||||
10
TODO.md
@@ -1,5 +1,5 @@
|
||||
# TODO
|
||||
List of TODO items for Caravel
|
||||
List of TODO items for Superset
|
||||
|
||||
## Important
|
||||
* **Getting proper JS testing:** unit tests on the Python side are pretty
|
||||
@@ -7,14 +7,12 @@ List of TODO items for Caravel
|
||||
testing all the ajax-type calls
|
||||
* **Viz Plugins:** Allow people to define and share visualization plugins.
|
||||
ideally one would only need to drop in a set of files in a folder and
|
||||
Caravel would discover and expose the plugins
|
||||
Superset would discover and expose the plugins
|
||||
|
||||
## Features
|
||||
* **Dashboard URL filters:** `{dash_url}#fltin__fieldname__value1,value2`
|
||||
* **Default slice:** choose a default slice for the dataset instead of
|
||||
default endpoint
|
||||
* **refresh freq**: specifying the refresh frequency of a dashboard and
|
||||
specific slices within it, some randomization would be nice
|
||||
* **Widget sets / chart grids:** a way to have all charts support making
|
||||
a series of charts and putting them in a grid. The same way that you
|
||||
can groupby for series, you could chart by. The form field set would be
|
||||
@@ -27,18 +25,14 @@ List of TODO items for Caravel
|
||||
some visualizations as annotations. An example of a layer might be
|
||||
"holidays" or "site outages", ...
|
||||
* **Slack integration** - TBD
|
||||
* **Sexy Viz Selector:** the visualization selector should be a nice large
|
||||
modal with nice thumbnails for each one of the viz
|
||||
* **Comments:** allow for people to comment on slices and dashes
|
||||
|
||||
|
||||
## Easy-ish fix
|
||||
* Build matrix to include mysql using tox
|
||||
* Kill switch for Druid in docs
|
||||
* CREATE VIEW button from SQL editor
|
||||
* Test button for when editing SQL expression
|
||||
* Slider form element
|
||||
* datasource in explore mode could be a dropdown
|
||||
* [druid] Allow for post aggregations (ratios!)
|
||||
* in/notin filters autocomplete (druid)
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ script_location = migrations
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = scheme://localhost/caravel
|
||||
sqlalchemy.url = scheme://localhost/superset
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
|
||||
4
babel/babel.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
[ignore: superset/assets/node_modules/**]
|
||||
[python: superset/**.py]
|
||||
[jinja2: superset/**/templates/**.html]
|
||||
encoding = utf-8
|
||||
1809
babel/messages.pot
Executable file
@@ -1,48 +0,0 @@
|
||||
"""Package's main module!"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from flask import Flask, redirect
|
||||
from flask.ext.appbuilder import SQLA, AppBuilder, IndexView
|
||||
from flask.ext.appbuilder.baseviews import expose
|
||||
from flask.ext.cache import Cache
|
||||
from flask.ext.migrate import Migrate
|
||||
|
||||
VERSION = '0.8.8'
|
||||
|
||||
APP_DIR = os.path.dirname(__file__)
|
||||
CONFIG_MODULE = os.environ.get('CARAVEL_CONFIG', 'caravel.config')
|
||||
|
||||
# Logging configuration
|
||||
logging.basicConfig(format='%(asctime)s:%(levelname)s:%(name)s:%(message)s')
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(CONFIG_MODULE)
|
||||
db = SQLA(app)
|
||||
|
||||
cache = Cache(app, config=app.config.get('CACHE_CONFIG'))
|
||||
|
||||
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")
|
||||
|
||||
|
||||
class MyIndexView(IndexView):
|
||||
@expose('/')
|
||||
def index(self):
|
||||
return redirect('/caravel/welcome')
|
||||
|
||||
appbuilder = AppBuilder(
|
||||
app, db.session,
|
||||
base_template='caravel/base.html',
|
||||
indexview=MyIndexView,
|
||||
security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER"))
|
||||
|
||||
sm = appbuilder.sm
|
||||
|
||||
get_session = appbuilder.get_session
|
||||
from caravel import config, views # noqa
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets" : ["es2015", "react"]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
node_modules/*
|
||||
vendor/*
|
||||
javascripts/dist/*
|
||||
@@ -1,234 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
|
||||
"globals": {
|
||||
"Symbol": false,
|
||||
"Map": false,
|
||||
"Set": false,
|
||||
"Reflect": false,
|
||||
},
|
||||
|
||||
"env": {
|
||||
"es6": false,
|
||||
"browser": true,
|
||||
"node": true,
|
||||
},
|
||||
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 5,
|
||||
"sourceType": "module"
|
||||
},
|
||||
|
||||
"rules": {
|
||||
"array-bracket-spacing": [2, "never", {
|
||||
"singleValue": false,
|
||||
"objectsInArrays": false,
|
||||
"arraysInArrays": false
|
||||
}],
|
||||
"array-callback-return": [2],
|
||||
"block-spacing": [2, "always"],
|
||||
"brace-style": [2, "1tbs", { "allowSingleLine": true }],
|
||||
"callback-return": [2, ["callback"]],
|
||||
"camelcase": [0],
|
||||
"comma-dangle": [2, "never"],
|
||||
"comma-spacing": [2],
|
||||
"comma-style": [2, "last"],
|
||||
"curly": [2, "all"],
|
||||
"eqeqeq": 2,
|
||||
"func-names": [0],
|
||||
"id-length": [2, { "min": 1, "max": 25, "properties": "never" }],
|
||||
"key-spacing": [2, { "beforeColon": false, "afterColon": true }],
|
||||
"keyword-spacing": [2, {
|
||||
"before": true,
|
||||
"after": true,
|
||||
"overrides": {
|
||||
"return": { "after": true },
|
||||
"throw": { "after": true },
|
||||
"case": { "after": true }
|
||||
}
|
||||
}],
|
||||
"linebreak-style": [2, "unix"],
|
||||
"lines-around-comment": [2, {
|
||||
"beforeBlockComment": false,
|
||||
"afterBlockComment": false,
|
||||
"beforeLineComment": false,
|
||||
"allowBlockStart": true,
|
||||
"allowBlockEnd": true
|
||||
}],
|
||||
"max-depth": [2, 5],
|
||||
"max-len": [0, 80, 4],
|
||||
"max-nested-callbacks": [1, 3],
|
||||
"max-params": [1, 4],
|
||||
"new-parens": [2],
|
||||
"newline-after-var": [0],
|
||||
"no-bitwise": [0],
|
||||
"no-cond-assign": [2],
|
||||
"no-console": [1, { allow: ["warn", "error"] }],
|
||||
"no-const-assign": [2],
|
||||
"no-constant-condition": [2],
|
||||
"no-control-regex": [2],
|
||||
"no-debugger": [2],
|
||||
"no-delete-var": [2],
|
||||
"no-dupe-args": [2],
|
||||
"no-dupe-class-members": [2],
|
||||
"no-dupe-keys": [2],
|
||||
"no-duplicate-case": [2],
|
||||
"no-else-return": [0],
|
||||
"no-empty": [2],
|
||||
"no-eq-null": [0],
|
||||
"no-eval": [2],
|
||||
"no-ex-assign": [2],
|
||||
"no-extend-native": [2],
|
||||
"no-extra-bind": [2],
|
||||
"no-extra-boolean-cast": [2],
|
||||
"no-extra-label": [2],
|
||||
"no-extra-parens": [0], // needed for clearer #math eg (a - b) / c
|
||||
"no-extra-semi": [2],
|
||||
"no-fallthrough": [2],
|
||||
"no-floating-decimal": [2],
|
||||
"no-func-assign": [2],
|
||||
"no-implied-eval": [2],
|
||||
"no-implicit-coercion": [2, {
|
||||
"boolean": false,
|
||||
"number": true,
|
||||
"string": true
|
||||
}],
|
||||
"no-implicit-globals": [2],
|
||||
"no-inline-comments": [0],
|
||||
"no-invalid-regexp": [2],
|
||||
"no-irregular-whitespace": [2],
|
||||
"no-iterator": [2],
|
||||
"no-label-var": [2],
|
||||
"no-labels": [2, { "allowLoop": false, "allowSwitch": false }],
|
||||
"no-lone-blocks": [2],
|
||||
"no-lonely-if": [2],
|
||||
"no-loop-func": [2],
|
||||
"no-magic-numbers": [0], // doesn't work well with vis cosmetic constant
|
||||
"no-mixed-requires": [1, false],
|
||||
"no-mixed-spaces-and-tabs": [2, false],
|
||||
"no-multi-spaces": [2, {
|
||||
"exceptions": {
|
||||
"ImportDeclaration": true,
|
||||
"Property": true,
|
||||
"VariableDeclarator": true
|
||||
}
|
||||
}],
|
||||
"no-multi-str": [2],
|
||||
"no-multiple-empty-lines": [2, { "max": 1, "maxEOF": 1 }],
|
||||
"no-native-reassign": [2],
|
||||
"no-negated-condition": [2],
|
||||
"no-negated-in-lhs": [2],
|
||||
"no-nested-ternary": [0],
|
||||
"no-new": [2],
|
||||
"no-new-func": [2],
|
||||
"no-new-object": [2],
|
||||
"no-new-require": [0],
|
||||
"no-new-symbol": [2],
|
||||
"no-new-wrappers": [2],
|
||||
"no-obj-calls": [2],
|
||||
"no-octal": [2],
|
||||
"no-octal-escape": [2],
|
||||
"no-path-concat": [0],
|
||||
"no-process-env": [0],
|
||||
"no-process-exit": [2],
|
||||
"no-proto": [2],
|
||||
"no-redeclare": [2],
|
||||
"no-regex-spaces": [2],
|
||||
"no-restricted-modules": [0],
|
||||
"no-restricted-imports": [0],
|
||||
"no-restricted-syntax": [2,
|
||||
"DebuggerStatement",
|
||||
"LabeledStatement",
|
||||
"WithStatement"
|
||||
],
|
||||
"no-return-assign": [2, "always"],
|
||||
"no-script-url": [2],
|
||||
"no-self-assign": [2],
|
||||
"no-self-compare": [0],
|
||||
"no-sequences": [2],
|
||||
"no-shadow-restricted-names": [2],
|
||||
"no-spaced-func": [2],
|
||||
"no-sparse-arrays": [2],
|
||||
"no-sync": [0],
|
||||
"no-ternary": [0],
|
||||
"no-this-before-super": [2],
|
||||
"no-throw-literal": [2],
|
||||
"no-trailing-spaces": [2, { "skipBlankLines": false }],
|
||||
"no-undef": [2, { "typeof": true }],
|
||||
"no-undef-init": [2],
|
||||
"no-undefined": [0],
|
||||
"no-underscore-dangle": [0], // __data__ sometimes
|
||||
"no-unexpected-multiline": [2],
|
||||
"no-unmodified-loop-condition": [2],
|
||||
"no-unneeded-ternary": [2],
|
||||
"no-unreachable": [2],
|
||||
"no-unused-expressions": [2],
|
||||
"no-unused-labels": [2],
|
||||
"no-unused-vars": [2, {
|
||||
"vars": "all",
|
||||
"args": "none", // (d, i) pattern d3 func makes difficult to enforce
|
||||
"varsIgnorePattern": "jQuery"
|
||||
}],
|
||||
"no-use-before-define": [0],
|
||||
"no-useless-call": [2],
|
||||
"no-useless-concat": [2],
|
||||
"no-useless-constructor": [2],
|
||||
"no-void": [0],
|
||||
"no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }],
|
||||
"no-with": [2],
|
||||
"no-whitespace-before-property": [2],
|
||||
"object-curly-spacing": [2, "always"],
|
||||
"object-shorthand": [2, "never"],
|
||||
"one-var": [0],
|
||||
"one-var-declaration-per-line": [2, "initializations"],
|
||||
"operator-assignment": [0, "always"],
|
||||
"padded-blocks": [0],
|
||||
"prefer-arrow-callback": [0],
|
||||
"prefer-const": [0],
|
||||
"prefer-reflect": [0],
|
||||
"prefer-rest-params": [0],
|
||||
"prefer-spread": [0],
|
||||
"prefer-template": [0],
|
||||
"quote-props": [2, "as-needed", { "keywords": true }],
|
||||
"radix": [2],
|
||||
"require-yield": [2],
|
||||
"semi": [2],
|
||||
"semi-spacing": [2, { "before": false, "after": true }],
|
||||
"sort-vars": [0],
|
||||
"sort-imports": [0],
|
||||
"space-before-function-paren": [2, { "anonymous": "always", "named": "never" }],
|
||||
"space-before-blocks": [2, { "functions": "always", "keywords": "always" }],
|
||||
"space-in-brackets": [0, "never", {
|
||||
"singleValue": true,
|
||||
"arraysInArrays": false,
|
||||
"arraysInObjects": false,
|
||||
"objectsInArrays": true,
|
||||
"objectsInObjects": true,
|
||||
"propertyName": false
|
||||
}],
|
||||
},
|
||||
// Temporarily not enforced
|
||||
"new-cap": [2], // @TODO more tricky for the moment
|
||||
"newline-per-chained-call": [2, { "ignoreChainWithDepth": 6 }],
|
||||
"no-param-reassign": [0], // turn on once default args supported
|
||||
"no-shadow": [2, { // @TODO more tricky for the moment with eg 'data'
|
||||
"builtinGlobals": false,
|
||||
"hoist": "functions",
|
||||
"allow": ["i", "d"]
|
||||
}],
|
||||
"space-in-parens": [2, "never"],
|
||||
"space-infix-ops": [2],
|
||||
"space-unary-ops": [2, { "words": true, "nonwords": false }],
|
||||
"spaced-comment": [2, "always", { "markers": ["!"] }],
|
||||
"spaced-line-comment": [0, "always"],
|
||||
"strict": [2, "global"],
|
||||
"template-curly-spacing": [2, "never"],
|
||||
"use-isnan": [2],
|
||||
"valid-jsdoc": [0],
|
||||
"valid-typeof": [2],
|
||||
"vars-on-top": [0],
|
||||
"wrap-iife": [2],
|
||||
"wrap-regex": [2],
|
||||
"yield-star-spacing": [2, { "before": false, "after": true }],
|
||||
"yoda": [2, "never", { "exceptRange": true, "onlyEquality": false }]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 147 KiB |
@@ -1 +0,0 @@
|
||||
require('../stylesheets/less/index.less');
|
||||
@@ -1,258 +0,0 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var px = require('./modules/caravel.js');
|
||||
var d3 = require('d3');
|
||||
var showModal = require('./modules/utils.js').showModal;
|
||||
require('bootstrap');
|
||||
|
||||
var ace = require('brace');
|
||||
require('brace/mode/css');
|
||||
require('brace/theme/crimson_editor');
|
||||
|
||||
require('./caravel-select2.js');
|
||||
require('../node_modules/gridster/dist/jquery.gridster.min.css');
|
||||
require('../node_modules/gridster/dist/jquery.gridster.min.js');
|
||||
|
||||
var Dashboard = function (dashboardData) {
|
||||
var dashboard = $.extend(dashboardData, {
|
||||
filters: {},
|
||||
init: function () {
|
||||
this.initDashboardView();
|
||||
px.initFavStars();
|
||||
var sliceObjects = [],
|
||||
dash = this;
|
||||
dashboard.slices.forEach(function (data) {
|
||||
if (data.error) {
|
||||
var html = '<div class="alert alert-danger">' + data.error + '</div>';
|
||||
$("#slice_" + data.slice_id).find('.token').html(html);
|
||||
} else {
|
||||
var slice = px.Slice(data, dash);
|
||||
$("#slice_" + data.slice_id).find('a.refresh').click(function () {
|
||||
slice.render(true);
|
||||
});
|
||||
sliceObjects.push(slice);
|
||||
slice.render();
|
||||
}
|
||||
});
|
||||
this.slices = sliceObjects;
|
||||
},
|
||||
setFilter: function (slice_id, col, vals) {
|
||||
this.addFilter(slice_id, col, vals, false);
|
||||
},
|
||||
addFilter: function (slice_id, col, vals, merge) {
|
||||
if (merge === undefined) {
|
||||
merge = true;
|
||||
}
|
||||
if (!(slice_id in this.filters)) {
|
||||
this.filters[slice_id] = {};
|
||||
}
|
||||
if (!(col in this.filters[slice_id]) || !merge) {
|
||||
this.filters[slice_id][col] = vals;
|
||||
} else {
|
||||
this.filters[slice_id][col] = d3.merge([this.filters[slice_id][col], vals]);
|
||||
}
|
||||
this.refreshExcept(slice_id);
|
||||
},
|
||||
readFilters: function () {
|
||||
// Returns a list of human readable active filters
|
||||
return JSON.stringify(this.filters, null, 4);
|
||||
},
|
||||
refreshExcept: function (slice_id) {
|
||||
var immune = this.metadata.filter_immune_slice || [];
|
||||
this.slices.forEach(function (slice) {
|
||||
if (slice.data.slice_id !== slice_id && immune.indexOf(slice.data.slice_id) === -1) {
|
||||
slice.render();
|
||||
}
|
||||
});
|
||||
},
|
||||
clearFilters: function (slice_id) {
|
||||
delete this.filters[slice_id];
|
||||
this.refreshExcept(slice_id);
|
||||
},
|
||||
removeFilter: function (slice_id, col, vals) {
|
||||
if (slice_id in this.filters) {
|
||||
if (col in this.filters[slice_id]) {
|
||||
var a = [];
|
||||
this.filters[slice_id][col].forEach(function (v) {
|
||||
if (vals.indexOf(v) < 0) {
|
||||
a.push(v);
|
||||
}
|
||||
});
|
||||
this.filters[slice_id][col] = a;
|
||||
}
|
||||
}
|
||||
this.refreshExcept(slice_id);
|
||||
},
|
||||
getSlice: function (slice_id) {
|
||||
slice_id = parseInt(slice_id, 10);
|
||||
for (var i=0; i < this.slices.length; i++) {
|
||||
if (this.slices[i].data.slice_id === slice_id) {
|
||||
return this.slices[i];
|
||||
}
|
||||
}
|
||||
},
|
||||
initDashboardView: function () {
|
||||
dashboard = this;
|
||||
var gridster = $(".gridster ul").gridster({
|
||||
autogrow_cols: true,
|
||||
widget_margins: [10, 10],
|
||||
widget_base_dimensions: [95, 95],
|
||||
draggable: {
|
||||
handle: '.drag'
|
||||
},
|
||||
resize: {
|
||||
enabled: true,
|
||||
stop: function (e, ui, element) {
|
||||
dashboard.getSlice($(element).attr('slice_id')).resize();
|
||||
}
|
||||
},
|
||||
serialize_params: function (_w, wgd) {
|
||||
return {
|
||||
slice_id: $(_w).attr('slice_id'),
|
||||
col: wgd.col,
|
||||
row: wgd.row,
|
||||
size_x: wgd.size_x,
|
||||
size_y: wgd.size_y
|
||||
};
|
||||
}
|
||||
}).data('gridster');
|
||||
|
||||
// Displaying widget controls on hover
|
||||
$('.chart-header').hover(
|
||||
function () {
|
||||
$(this).find('.chart-controls').fadeIn(300);
|
||||
},
|
||||
function () {
|
||||
$(this).find('.chart-controls').fadeOut(300);
|
||||
}
|
||||
);
|
||||
$("div.gridster").css('visibility', 'visible');
|
||||
$("#savedash").click(function () {
|
||||
var expanded_slices = {};
|
||||
$.each($(".slice_info"), function (i, d) {
|
||||
var widget = $(this).parents('.widget');
|
||||
var slice_description = widget.find('.slice_description');
|
||||
if (slice_description.is(":visible")) {
|
||||
expanded_slices[$(d).attr('slice_id')] = true;
|
||||
}
|
||||
});
|
||||
var data = {
|
||||
positions: gridster.serialize(),
|
||||
css: editor.getValue(),
|
||||
expanded_slices: expanded_slices
|
||||
};
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '/caravel/save_dash/' + dashboard.id + '/',
|
||||
data: {
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
success: function () {
|
||||
showModal({
|
||||
title: "Success",
|
||||
body: "This dashboard was saved successfully."
|
||||
});
|
||||
},
|
||||
error: function (error) {
|
||||
showModal({
|
||||
title: "Error",
|
||||
body: "Sorry, there was an error saving this dashboard:<br />" + error
|
||||
});
|
||||
console.warn("Save dashboard error", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
var editor = ace.edit("dash_css");
|
||||
editor.$blockScrolling = Infinity;
|
||||
|
||||
editor.setTheme("ace/theme/crimson_editor");
|
||||
editor.setOptions({
|
||||
minLines: 16,
|
||||
maxLines: Infinity,
|
||||
useWorker: false
|
||||
});
|
||||
editor.getSession().setMode("ace/mode/css");
|
||||
|
||||
$(".select2").select2({
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
$("#css_template").on("change", function () {
|
||||
var css = $(this).find('option:selected').data('css');
|
||||
editor.setValue(css);
|
||||
|
||||
$('#dash_css').val(css);
|
||||
injectCss("dashboard-template", css);
|
||||
|
||||
});
|
||||
$('#filters').click(function () {
|
||||
showModal({
|
||||
title: "<span class='fa fa-info-circle'></span> Current Global Filters",
|
||||
body: "The following global filters are currently applied:<br/>" + dashboard.readFilters()
|
||||
});
|
||||
});
|
||||
$('#refresh_dash').click(function () {
|
||||
dashboard.slices.forEach(function (slice) {
|
||||
slice.render(true);
|
||||
});
|
||||
});
|
||||
$("a.remove-chart").click(function () {
|
||||
var li = $(this).parents("li");
|
||||
gridster.remove_widget(li);
|
||||
});
|
||||
|
||||
$("li.widget").click(function (e) {
|
||||
var $this = $(this);
|
||||
var $target = $(e.target);
|
||||
|
||||
if ($target.hasClass("slice_info")) {
|
||||
$this.find(".slice_description").slideToggle(0, function () {
|
||||
$this.find('.refresh').click();
|
||||
});
|
||||
} else if ($target.hasClass("controls-toggle")) {
|
||||
$this.find(".chart-controls").toggle();
|
||||
}
|
||||
});
|
||||
|
||||
editor.on("change", function () {
|
||||
var css = editor.getValue();
|
||||
$('#dash_css').val(css);
|
||||
injectCss("dashboard-template", css);
|
||||
});
|
||||
|
||||
var css = $('.dashboard').data('css');
|
||||
injectCss("dashboard-template", css);
|
||||
|
||||
// Injects the passed css string into a style sheet with the specified className
|
||||
// If a stylesheet doesn't exist with the passed className, one will be injected into <head>
|
||||
function injectCss(className, css) {
|
||||
|
||||
var head = document.head || document.getElementsByTagName('head')[0];
|
||||
var style = document.querySelector('.' + className);
|
||||
|
||||
if (!style) {
|
||||
if (className.split(' ').length > 1) {
|
||||
throw new Error("This method only supports selections with a single class name.");
|
||||
}
|
||||
style = document.createElement('style');
|
||||
style.className = className;
|
||||
style.type = 'text/css';
|
||||
head.appendChild(style);
|
||||
}
|
||||
|
||||
if (style.styleSheet) {
|
||||
style.styleSheet.cssText = css;
|
||||
} else {
|
||||
style.innerHTML = css;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
dashboard.init();
|
||||
return dashboard;
|
||||
};
|
||||
|
||||
$(document).ready(function () {
|
||||
Dashboard($('.dashboard').data('dashboard'));
|
||||
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
|
||||
});
|
||||
@@ -1,365 +0,0 @@
|
||||
// Javascript for the explorer page
|
||||
// Init explorer view -> load vis dependencies -> read data (from dynamic html) -> render slice
|
||||
// nb: to add a new vis, you must also add a Python fn in viz.py
|
||||
//
|
||||
// js
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var px = require('./modules/caravel.js');
|
||||
var showModal = require('./modules/utils.js').showModal;
|
||||
|
||||
require('jquery-ui');
|
||||
$.widget.bridge('uitooltip', $.ui.tooltip); // Shutting down jq-ui tooltips
|
||||
require('bootstrap');
|
||||
|
||||
require('./caravel-select2.js');
|
||||
|
||||
require('../node_modules/bootstrap-toggle/js/bootstrap-toggle.min.js');
|
||||
|
||||
// css
|
||||
require('../vendor/pygments.css');
|
||||
require('../node_modules/bootstrap-toggle/css/bootstrap-toggle.min.css');
|
||||
|
||||
var slice;
|
||||
|
||||
function prepForm() {
|
||||
var i = 1;
|
||||
// Assigning the right id to form elements in filters
|
||||
$("#filters > div").each(function () {
|
||||
$(this).attr("id", function () {
|
||||
return "flt_" + i;
|
||||
});
|
||||
$(this).find("#flt_col_0")
|
||||
.attr("id", function () {
|
||||
return "flt_col_" + i;
|
||||
})
|
||||
.attr("name", function () {
|
||||
return "flt_col_" + i;
|
||||
});
|
||||
$(this).find("#flt_op_0")
|
||||
.attr("id", function () {
|
||||
return "flt_op_" + i;
|
||||
})
|
||||
.attr("name", function () {
|
||||
return "flt_op_" + i;
|
||||
});
|
||||
$(this).find("#flt_eq_0")
|
||||
.attr("id", function () {
|
||||
return "flt_eq_" + i;
|
||||
})
|
||||
.attr("name", function () {
|
||||
return "flt_eq_" + i;
|
||||
});
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
function query(force, pushState) {
|
||||
if (force === undefined) {
|
||||
force = false;
|
||||
}
|
||||
if (pushState !== false) {
|
||||
history.pushState({}, document.title, slice.querystring());
|
||||
}
|
||||
$('.query-and-save button').attr('disabled', 'disabled');
|
||||
$('.btn-group.results span,a').attr('disabled', 'disabled');
|
||||
$('div.alert').remove();
|
||||
$('#is_cached').hide();
|
||||
prepForm();
|
||||
slice.render(force);
|
||||
}
|
||||
|
||||
function initExploreView() {
|
||||
|
||||
function get_collapsed_fieldsets() {
|
||||
var collapsed_fieldsets = $("#collapsed_fieldsets").val();
|
||||
|
||||
if (collapsed_fieldsets !== undefined && collapsed_fieldsets !== "") {
|
||||
collapsed_fieldsets = collapsed_fieldsets.split('||');
|
||||
} else {
|
||||
collapsed_fieldsets = [];
|
||||
}
|
||||
return collapsed_fieldsets;
|
||||
}
|
||||
|
||||
function toggle_fieldset(legend, animation) {
|
||||
var parent = legend.parent();
|
||||
var fieldset = parent.find(".legend_label").text();
|
||||
var collapsed_fieldsets = get_collapsed_fieldsets();
|
||||
var index;
|
||||
|
||||
if (parent.hasClass("collapsed")) {
|
||||
if (animation) {
|
||||
parent.find(".panel-body").slideDown();
|
||||
} else {
|
||||
parent.find(".panel-body").show();
|
||||
}
|
||||
parent.removeClass("collapsed");
|
||||
parent.find("span.collapser").text("[-]");
|
||||
|
||||
// removing from array, js is overcomplicated
|
||||
index = collapsed_fieldsets.indexOf(fieldset);
|
||||
if (index !== -1) {
|
||||
collapsed_fieldsets.splice(index, 1);
|
||||
}
|
||||
} else { // not collapsed
|
||||
if (animation) {
|
||||
parent.find(".panel-body").slideUp();
|
||||
} else {
|
||||
parent.find(".panel-body").hide();
|
||||
}
|
||||
|
||||
parent.addClass("collapsed");
|
||||
parent.find("span.collapser").text("[+]");
|
||||
index = collapsed_fieldsets.indexOf(fieldset);
|
||||
if (index === -1 && fieldset !== "" && fieldset !== undefined) {
|
||||
collapsed_fieldsets.push(fieldset);
|
||||
}
|
||||
}
|
||||
|
||||
$("#collapsed_fieldsets").val(collapsed_fieldsets.join("||"));
|
||||
}
|
||||
|
||||
px.initFavStars();
|
||||
|
||||
$('form .panel-heading').click(function () {
|
||||
toggle_fieldset($(this), true);
|
||||
$(this).css('cursor', 'pointer');
|
||||
});
|
||||
|
||||
function copyURLToClipboard(url) {
|
||||
var textArea = document.createElement("textarea");
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-1000px';
|
||||
textArea.value = url;
|
||||
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
if (!successful) {
|
||||
throw new Error("Not successful");
|
||||
}
|
||||
} catch (err) {
|
||||
window.alert("Sorry, your browser does not support copying. Use Ctrl / Cmd + C!");
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
return successful;
|
||||
}
|
||||
|
||||
$('#shortner').click(function () {
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '/r/shortner/',
|
||||
data: {
|
||||
data: '/' + window.location.pathname + slice.querystring()
|
||||
},
|
||||
success: function (data) {
|
||||
var close = '<a style="cursor: pointer;"><i class="fa fa-close" id="close_shortner"></i></a>';
|
||||
var copy = '<a style="cursor: pointer;"><i class="fa fa-clipboard" title="Copy to clipboard" id="copy_url"></i></a>';
|
||||
var spaces = ' ';
|
||||
var popover = data + spaces + copy + spaces + close;
|
||||
|
||||
var $shortner = $('#shortner')
|
||||
.popover({
|
||||
content: popover,
|
||||
placement: 'left',
|
||||
html: true,
|
||||
trigger: 'manual'
|
||||
})
|
||||
.popover('show');
|
||||
|
||||
$('#copy_url').tooltip().click(function () {
|
||||
var success = copyURLToClipboard(data);
|
||||
if (success) {
|
||||
$(this).attr("data-original-title", "Copied!").tooltip('fixTitle').tooltip('show');
|
||||
window.setTimeout(destroyPopover, 1200);
|
||||
}
|
||||
});
|
||||
$('#close_shortner').click(destroyPopover);
|
||||
|
||||
function destroyPopover() {
|
||||
$shortner.popover('destroy');
|
||||
}
|
||||
},
|
||||
error: function (error) {
|
||||
showModal({
|
||||
title: "Error",
|
||||
body: "Sorry, an error occurred during this operation:<br/>" + error
|
||||
});
|
||||
console.warn("Short URL error", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$("#viz_type").change(function () {
|
||||
$("#query").submit();
|
||||
});
|
||||
|
||||
$("#datasource_id").change(function () {
|
||||
var url = $(this).find('option:selected').attr('url');
|
||||
window.location = url;
|
||||
});
|
||||
|
||||
var collapsed_fieldsets = get_collapsed_fieldsets();
|
||||
for (var i = 0; i < collapsed_fieldsets.length; i++) {
|
||||
toggle_fieldset($('legend:contains("' + collapsed_fieldsets[i] + '")'), false);
|
||||
}
|
||||
function formatViz(viz) {
|
||||
var url = '/static/assets/images/viz_thumbnails/' + viz.id + '.png';
|
||||
var no_img = '/static/assets/images/noimg.png';
|
||||
return $(
|
||||
'<img class="viz-thumb-option" src="' + url + '" onerror="this.src=\'' + no_img + '\';">' +
|
||||
'<span>' + viz.text + '</span>'
|
||||
);
|
||||
}
|
||||
|
||||
$(".select2").select2({
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
$(".select2Sortable").select2({
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
$(".select2-with-images").select2({
|
||||
dropdownAutoWidth: true,
|
||||
dropdownCssClass: "bigdrop",
|
||||
formatResult: formatViz
|
||||
});
|
||||
$(".select2Sortable").select2Sortable({
|
||||
bindOrder: 'sortableStop'
|
||||
});
|
||||
$("form").show();
|
||||
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
|
||||
$(".ui-helper-hidden-accessible").remove(); // jQuery-ui 1.11+ creates a div for every tooltip
|
||||
|
||||
function set_filters() {
|
||||
for (var i = 1; i < 10; i++) {
|
||||
var eq = px.getParam("flt_eq_" + i);
|
||||
if (eq !== '') {
|
||||
add_filter(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
set_filters();
|
||||
|
||||
function add_filter(i) {
|
||||
var cp = $("#flt0").clone();
|
||||
$(cp).appendTo("#filters");
|
||||
$(cp).show();
|
||||
if (i !== undefined) {
|
||||
$(cp).find("#flt_eq_0").val(px.getParam("flt_eq_" + i));
|
||||
$(cp).find("#flt_op_0").val(px.getParam("flt_op_" + i));
|
||||
$(cp).find("#flt_col_0").val(px.getParam("flt_col_" + i));
|
||||
}
|
||||
$(cp).find('select').select2();
|
||||
$(cp).find('.remove').click(function () {
|
||||
$(this).parent().parent().remove();
|
||||
});
|
||||
}
|
||||
|
||||
$(window).bind("popstate", function (event) {
|
||||
// Browser back button
|
||||
var returnLocation = history.location || document.location;
|
||||
// Could do something more lightweight here, but we're not optimizing
|
||||
// for the use of the back button anyways
|
||||
returnLocation.reload();
|
||||
});
|
||||
|
||||
$("#plus").click(add_filter);
|
||||
$("#btn_save").click(function () {
|
||||
var slice_name = prompt("Name your slice!");
|
||||
if (slice_name !== "" && slice_name !== null) {
|
||||
$("#slice_name").val(slice_name);
|
||||
prepForm();
|
||||
$("#action").val("save");
|
||||
$("#query").submit();
|
||||
}
|
||||
});
|
||||
$("#btn_overwrite").click(function () {
|
||||
var flag = confirm("Overwrite slice [" + $("#slice_name").val() + "] !?");
|
||||
if (flag) {
|
||||
$("#action").val("overwrite");
|
||||
prepForm();
|
||||
$("#query").submit();
|
||||
}
|
||||
});
|
||||
|
||||
$(".query").click(function () {
|
||||
query(true);
|
||||
});
|
||||
|
||||
function create_choices(term, data) {
|
||||
var filtered = $(data).filter(function () {
|
||||
return this.text.localeCompare(term) === 0;
|
||||
});
|
||||
if (filtered.length === 0) {
|
||||
return {
|
||||
id: term,
|
||||
text: term
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function initSelectionToValue(element, callback) {
|
||||
callback({
|
||||
id: element.val(),
|
||||
text: element.val()
|
||||
});
|
||||
}
|
||||
|
||||
$(".select2_freeform").each(function () {
|
||||
var parent = $(this).parent();
|
||||
var name = $(this).attr('name');
|
||||
var l = [];
|
||||
var selected = '';
|
||||
for (var i = 0; i < this.options.length; i++) {
|
||||
l.push({
|
||||
id: this.options[i].value,
|
||||
text: this.options[i].text
|
||||
});
|
||||
if (this.options[i].selected) {
|
||||
selected = this.options[i].value;
|
||||
}
|
||||
}
|
||||
parent.append(
|
||||
'<input class="' + $(this).attr('class') + '" name="' + name + '" type="text" value="' + selected + '">'
|
||||
);
|
||||
$("input[name='" + name + "']").select2({
|
||||
createSearchChoice: create_choices,
|
||||
initSelection: initSelectionToValue,
|
||||
dropdownAutoWidth: true,
|
||||
multiple: false,
|
||||
data: l
|
||||
});
|
||||
$(this).remove();
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
initExploreView();
|
||||
|
||||
// Dynamically register this visualization
|
||||
var visType = window.viz_type.value;
|
||||
px.registerViz(visType);
|
||||
|
||||
var data = $('.slice').data('slice');
|
||||
slice = px.Slice(data);
|
||||
|
||||
//
|
||||
$('.slice').data('slice', slice);
|
||||
|
||||
// call vis render method, which issues ajax
|
||||
query(false, false);
|
||||
|
||||
// make checkbox inputs display as toggles
|
||||
$(':checkbox')
|
||||
.addClass('pull-right')
|
||||
.attr("data-onstyle", "default")
|
||||
.bootstrapToggle({
|
||||
size: 'mini'
|
||||
});
|
||||
|
||||
$('div.toggle').addClass('pull-right');
|
||||
slice.bindResizeToWindowResize();
|
||||
});
|
||||
@@ -1,18 +0,0 @@
|
||||
var $ = require('jquery');
|
||||
var jQuery = $;
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Jumbotron } from 'react-bootstrap';
|
||||
|
||||
class App extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<Jumbotron>
|
||||
<h1>Caravel</h1>
|
||||
<p>Extensible visualization tool for exploring data from any database.</p>
|
||||
</Jumbotron>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(<App />, document.getElementById('app'));
|
||||
@@ -1,386 +0,0 @@
|
||||
var $ = require('jquery');
|
||||
var jQuery = $;
|
||||
var d3 = require('d3');
|
||||
|
||||
// vis sources
|
||||
var sourceMap = {
|
||||
area: 'nvd3_vis.js',
|
||||
bar: 'nvd3_vis.js',
|
||||
bubble: 'nvd3_vis.js',
|
||||
big_number: 'big_number.js',
|
||||
big_number_total: 'big_number.js',
|
||||
compare: 'nvd3_vis.js',
|
||||
dist_bar: 'nvd3_vis.js',
|
||||
directed_force: 'directed_force.js',
|
||||
filter_box: 'filter_box.js',
|
||||
heatmap: 'heatmap.js',
|
||||
iframe: 'iframe.js',
|
||||
line: 'nvd3_vis.js',
|
||||
markup: 'markup.js',
|
||||
para: 'parallel_coordinates.js',
|
||||
pie: 'nvd3_vis.js',
|
||||
box_plot: 'nvd3_vis.js',
|
||||
pivot_table: 'pivot_table.js',
|
||||
sankey: 'sankey.js',
|
||||
sunburst: 'sunburst.js',
|
||||
table: 'table.js',
|
||||
word_cloud: 'word_cloud.js',
|
||||
world_map: 'world_map.js',
|
||||
treemap: 'treemap.js'
|
||||
};
|
||||
|
||||
var color = function () {
|
||||
// Color related utility functions go in this object
|
||||
var bnbColors = [
|
||||
//rausch hackb kazan babu lima beach barol
|
||||
'#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
|
||||
'#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a',
|
||||
'#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e'
|
||||
];
|
||||
var spectrums = {
|
||||
blue_white_yellow: ['#00d1c1', 'white', '#ffb400'],
|
||||
fire: ['white', 'yellow', 'red', 'black'],
|
||||
white_black: ['white', 'black'],
|
||||
black_white: ['black', 'white']
|
||||
};
|
||||
var colorBnb = function () {
|
||||
// Color factory
|
||||
var seen = {};
|
||||
return function (s) {
|
||||
if (!s) { return; }
|
||||
// next line is for caravel series that should have the same color
|
||||
s = s.replace('---', '');
|
||||
if (seen[s] === undefined) {
|
||||
seen[s] = Object.keys(seen).length;
|
||||
}
|
||||
return this.bnbColors[seen[s] % this.bnbColors.length];
|
||||
};
|
||||
};
|
||||
var colorScalerFactory = function (colors, data, accessor) {
|
||||
// Returns a linear scaler our of an array of color
|
||||
if (!Array.isArray(colors)) {
|
||||
colors = spectrums[colors];
|
||||
}
|
||||
|
||||
var ext = [0, 1];
|
||||
if (data !== undefined) {
|
||||
ext = d3.extent(data, accessor);
|
||||
}
|
||||
|
||||
var points = [];
|
||||
var chunkSize = (ext[1] - ext[0]) / colors.length;
|
||||
$.each(colors, function (i, c) {
|
||||
points.push(i * chunkSize);
|
||||
});
|
||||
return d3.scale.linear().domain(points).range(colors);
|
||||
};
|
||||
return {
|
||||
bnbColors: bnbColors,
|
||||
category21: colorBnb(),
|
||||
colorScalerFactory: colorScalerFactory
|
||||
};
|
||||
};
|
||||
|
||||
var px = (function () {
|
||||
|
||||
var visualizations = {};
|
||||
var slice;
|
||||
|
||||
function getParam(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
function UTC(dttm) {
|
||||
return new Date(dttm.getUTCFullYear(), dttm.getUTCMonth(), dttm.getUTCDate(), dttm.getUTCHours(), dttm.getUTCMinutes(), dttm.getUTCSeconds());
|
||||
}
|
||||
var tickMultiFormat = d3.time.format.multi([
|
||||
[".%L", function (d) {
|
||||
return d.getMilliseconds();
|
||||
}], // If there are millisections, show only them
|
||||
[":%S", function (d) {
|
||||
return d.getSeconds();
|
||||
}], // If there are seconds, show only them
|
||||
["%a %b %d, %I:%M %p", function (d) {
|
||||
return d.getMinutes() !== 0;
|
||||
}], // If there are non-zero minutes, show Date, Hour:Minute [AM/PM]
|
||||
["%a %b %d, %I %p", function (d) {
|
||||
return d.getHours() !== 0;
|
||||
}], // If there are hours that are multiples of 3, show date and AM/PM
|
||||
["%a %b %d, %Y", function (d) {
|
||||
return d.getDate() !== 1;
|
||||
}], // If not the first of the month, do "month day, year."
|
||||
["%B %Y", function (d) {
|
||||
return d.getMonth() !== 0 && d.getDate() === 1;
|
||||
}], // If the first of the month, do "month day, year."
|
||||
["%Y", function (d) {
|
||||
return true;
|
||||
}] // fall back on month, year
|
||||
]);
|
||||
|
||||
function formatDate(dttm) {
|
||||
var d = UTC(new Date(dttm));
|
||||
//d = new Date(d.getTime() - 1 * 60 * 60 * 1000);
|
||||
return tickMultiFormat(d);
|
||||
}
|
||||
|
||||
function timeFormatFactory(d3timeFormat) {
|
||||
var f = d3.time.format(d3timeFormat);
|
||||
return function (dttm) {
|
||||
var d = UTC(new Date(dttm));
|
||||
return f(d);
|
||||
};
|
||||
}
|
||||
|
||||
function initFavStars() {
|
||||
var baseUrl = '/caravel/favstar/';
|
||||
// Init star behavihor for favorite
|
||||
function show() {
|
||||
if ($(this).hasClass('selected')) {
|
||||
$(this).html('<i class="fa fa-star"></i>');
|
||||
} else {
|
||||
$(this).html('<i class="fa fa-star-o"></i>');
|
||||
}
|
||||
}
|
||||
$('.favstar')
|
||||
.attr('title', 'Click to favorite/unfavorite')
|
||||
.each(show)
|
||||
.each(function () {
|
||||
var url = baseUrl + $(this).attr("class_name");
|
||||
var star = this;
|
||||
url += '/' + $(this).attr("obj_id") + '/';
|
||||
$.getJSON(url + 'count/', function (data) {
|
||||
if (data.count > 0) {
|
||||
$(star)
|
||||
.addClass('selected')
|
||||
.each(show);
|
||||
}
|
||||
});
|
||||
})
|
||||
.click(function () {
|
||||
$(this).toggleClass('selected');
|
||||
var url = baseUrl + $(this).attr("class_name");
|
||||
url += '/' + $(this).attr("obj_id") + '/';
|
||||
if ($(this).hasClass('selected')) {
|
||||
url += 'select/';
|
||||
} else {
|
||||
url += 'unselect/';
|
||||
}
|
||||
$.get(url);
|
||||
$(this).each(show);
|
||||
})
|
||||
.tooltip();
|
||||
}
|
||||
|
||||
var Slice = function (data, dashboard) {
|
||||
var timer;
|
||||
var token = $('#' + data.token);
|
||||
var container_id = data.token + '_con';
|
||||
var selector = '#' + container_id;
|
||||
var container = $(selector);
|
||||
var slice_id = data.slice_id;
|
||||
var dttm = 0;
|
||||
var stopwatch = function () {
|
||||
dttm += 10;
|
||||
var num = dttm / 1000;
|
||||
$('#timer').text(num.toFixed(2) + " sec");
|
||||
};
|
||||
var qrystr = '';
|
||||
var always = function (data) {
|
||||
//Private f, runs after done and error
|
||||
clearInterval(timer);
|
||||
$('#timer').removeClass('btn-warning');
|
||||
};
|
||||
slice = {
|
||||
data: data,
|
||||
container: container,
|
||||
container_id: container_id,
|
||||
selector: selector,
|
||||
querystring: function () {
|
||||
var parser = document.createElement('a');
|
||||
parser.href = data.json_endpoint;
|
||||
if (dashboard !== undefined) {
|
||||
var flts = encodeURIComponent(JSON.stringify(dashboard.filters));
|
||||
qrystr = parser.search + "&extra_filters=" + flts;
|
||||
} else if ($('#query').length === 0) {
|
||||
qrystr = parser.search;
|
||||
} else {
|
||||
qrystr = '?' + $('#query').serialize();
|
||||
}
|
||||
return qrystr;
|
||||
},
|
||||
getWidgetHeader: function () {
|
||||
return this.container.parents("li.widget").find(".chart-header");
|
||||
},
|
||||
jsonEndpoint: function () {
|
||||
var parser = document.createElement('a');
|
||||
parser.href = data.json_endpoint;
|
||||
var endpoint = parser.pathname + this.querystring();
|
||||
endpoint += "&json=true";
|
||||
endpoint += "&force=" + this.force;
|
||||
return endpoint;
|
||||
},
|
||||
done: function (data) {
|
||||
clearInterval(timer);
|
||||
token.find("img.loading").hide();
|
||||
container.show();
|
||||
|
||||
var cachedSelector = null;
|
||||
if (dashboard === undefined) {
|
||||
cachedSelector = $('#is_cached');
|
||||
if (data !== undefined && data.is_cached) {
|
||||
cachedSelector
|
||||
.attr('title', 'Served from data cached at ' + data.cached_dttm + '. Click to force-refresh')
|
||||
.show()
|
||||
.tooltip('fixTitle');
|
||||
} else {
|
||||
cachedSelector.hide();
|
||||
}
|
||||
} else {
|
||||
var refresh = this.getWidgetHeader().find('.refresh');
|
||||
if (data !== undefined && data.is_cached) {
|
||||
refresh
|
||||
.addClass('danger')
|
||||
.attr(
|
||||
'title',
|
||||
'Served from data cached at ' + data.cached_dttm + '. Click to force-refresh')
|
||||
.tooltip('fixTitle');
|
||||
} else {
|
||||
refresh
|
||||
.removeClass('danger')
|
||||
.attr(
|
||||
'title',
|
||||
'Click to force-refresh')
|
||||
.tooltip('fixTitle');
|
||||
}
|
||||
}
|
||||
if (data !== undefined) {
|
||||
$("#query_container").html(data.query);
|
||||
}
|
||||
$('#timer').removeClass('btn-warning');
|
||||
$('#timer').addClass('btn-success');
|
||||
$('span.query').removeClass('disabled');
|
||||
$('#json').click(function () {
|
||||
window.location = data.json_endpoint;
|
||||
});
|
||||
$('#standalone').click(function () {
|
||||
window.location = data.standalone_endpoint;
|
||||
});
|
||||
$('#csv').click(function () {
|
||||
window.location = data.csv_endpoint;
|
||||
});
|
||||
$('.btn-group.results span,a').removeAttr('disabled');
|
||||
$('.query-and-save button').removeAttr('disabled');
|
||||
always(data);
|
||||
},
|
||||
error: function (msg) {
|
||||
token.find("img.loading").hide();
|
||||
var err = '<div class="alert alert-danger">' + msg + '</div>';
|
||||
container.html(err);
|
||||
container.show();
|
||||
$('span.query').removeClass('disabled');
|
||||
$('#timer').addClass('btn-danger');
|
||||
$('.btn-group.results span,a').removeAttr('disabled');
|
||||
$('.query-and-save button').removeAttr('disabled');
|
||||
always(data);
|
||||
},
|
||||
width: function () {
|
||||
return token.width();
|
||||
},
|
||||
height: function () {
|
||||
var others = 0;
|
||||
var widget = container.parents('.widget');
|
||||
var slice_description = widget.find('.slice_description');
|
||||
if (slice_description.is(":visible")) {
|
||||
others += widget.find('.slice_description').height() + 25;
|
||||
}
|
||||
others += widget.find('.chart-header').height();
|
||||
return widget.height() - others - 10;
|
||||
},
|
||||
bindResizeToWindowResize: function () {
|
||||
var resizeTimer;
|
||||
$(window).on('resize', function (e) {
|
||||
clearTimeout(resizeTimer);
|
||||
resizeTimer = setTimeout(function () {
|
||||
slice.resize();
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
render: function (force) {
|
||||
if (force === undefined) {
|
||||
force = false;
|
||||
}
|
||||
this.force = force;
|
||||
token.find("img.loading").show();
|
||||
container.hide();
|
||||
container.html('');
|
||||
container.css('height', slice.height());
|
||||
dttm = 0;
|
||||
timer = setInterval(stopwatch, 10);
|
||||
$('#timer').removeClass('btn-danger btn-success');
|
||||
$('#timer').addClass('btn-warning');
|
||||
this.viz.render();
|
||||
},
|
||||
resize: function () {
|
||||
token.find("img.loading").show();
|
||||
container.hide();
|
||||
container.css('height', slice.height());
|
||||
container.html('');
|
||||
this.viz.render();
|
||||
this.viz.resize();
|
||||
},
|
||||
addFilter: function (col, vals) {
|
||||
if (dashboard !== undefined) {
|
||||
dashboard.addFilter(slice_id, col, vals);
|
||||
}
|
||||
},
|
||||
setFilter: function (col, vals) {
|
||||
if (dashboard !== undefined) {
|
||||
dashboard.setFilter(slice_id, col, vals);
|
||||
}
|
||||
},
|
||||
clearFilter: function () {
|
||||
if (dashboard !== undefined) {
|
||||
delete dashboard.clearFilter(slice_id);
|
||||
}
|
||||
},
|
||||
removeFilter: function (col, vals) {
|
||||
if (dashboard !== undefined) {
|
||||
delete dashboard.removeFilter(slice_id, col, vals);
|
||||
}
|
||||
}
|
||||
};
|
||||
var visType = data.form_data.viz_type;
|
||||
px.registerViz(visType);
|
||||
slice.viz = visualizations[data.form_data.viz_type](slice);
|
||||
return slice;
|
||||
};
|
||||
|
||||
function registerViz(name) {
|
||||
var visSource = sourceMap[name];
|
||||
|
||||
if (visSource) {
|
||||
var visFactory = require('../../visualizations/' + visSource);
|
||||
if (typeof visFactory === 'function') {
|
||||
visualizations[name] = visFactory;
|
||||
}
|
||||
} else {
|
||||
throw new Error("require(" + name + ") failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// Export public functions
|
||||
return {
|
||||
registerViz: registerViz,
|
||||
Slice: Slice,
|
||||
formatDate: formatDate,
|
||||
timeFormatFactory: timeFormatFactory,
|
||||
color: color(),
|
||||
getParam: getParam,
|
||||
initFavStars: initFavStars
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = px;
|
||||
@@ -1,80 +0,0 @@
|
||||
var $ = require('jquery');
|
||||
var d3 = require('d3');
|
||||
|
||||
/*
|
||||
Utility function that takes a d3 svg:text selection and a max width, and splits the
|
||||
text's text across multiple tspan lines such that any given line does not exceed max width
|
||||
|
||||
If text does not span multiple lines AND adjustedY is passed, will set the text to the passed val
|
||||
*/
|
||||
function wrapSvgText(text, width, adjustedY) {
|
||||
var lineHeight = 1; // ems
|
||||
|
||||
text.each(function () {
|
||||
var text = d3.select(this),
|
||||
words = text.text().split(/\s+/),
|
||||
word,
|
||||
line = [],
|
||||
lineNumber = 0,
|
||||
x = text.attr("x"),
|
||||
y = text.attr("y"),
|
||||
dy = parseFloat(text.attr("dy")),
|
||||
tspan = text.text(null)
|
||||
.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", dy + "em");
|
||||
|
||||
var didWrap = false;
|
||||
|
||||
for (var i = 0; i < words.length; i++) {
|
||||
word = words[i];
|
||||
line.push(word);
|
||||
tspan.text(line.join(" "));
|
||||
|
||||
if (tspan.node().getComputedTextLength() > width) {
|
||||
line.pop(); // remove word that pushes over the limit
|
||||
tspan.text(line.join(" "));
|
||||
line = [word];
|
||||
tspan = text.append("tspan")
|
||||
.attr("x", x)
|
||||
.attr("y", y)
|
||||
.attr("dy", ++lineNumber * lineHeight + dy + "em")
|
||||
.text(word);
|
||||
|
||||
didWrap = true;
|
||||
}
|
||||
}
|
||||
if (!didWrap && typeof adjustedY !== "undefined") {
|
||||
tspan.attr("y", adjustedY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the body and title content of a modal, and shows it. Assumes HTML for modal exists and that
|
||||
* it handles closing (i.e., works with bootstrap)
|
||||
*
|
||||
* @param {object} options object of the form
|
||||
* {
|
||||
* title: {string},
|
||||
* body: {string},
|
||||
* modalSelector: {string, default: '.misc-modal' },
|
||||
* titleSelector: {string, default: '.misc-modal .modal-title' },
|
||||
* bodySelector: {string, default: '.misc-modal .modal-body' },
|
||||
* }
|
||||
*/
|
||||
function showModal(options) {
|
||||
options.modalSelector = options.modalSelector || ".misc-modal";
|
||||
options.titleSelector = options.titleSelector || ".misc-modal .modal-title";
|
||||
options.bodySelector = options.bodySelector || ".misc-modal .modal-body";
|
||||
|
||||
$(options.titleSelector).html(options.title || "");
|
||||
$(options.bodySelector).html(options.body || "");
|
||||
$(options.modalSelector).modal("show");
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
wrapSvgText: wrapSvgText,
|
||||
showModal: showModal
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var showModal = require('./modules/utils.js').showModal;
|
||||
|
||||
require('./caravel-select2.js');
|
||||
|
||||
require('datatables.net-bs');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
require('bootstrap');
|
||||
|
||||
var ace = require('brace');
|
||||
require('brace/mode/sql');
|
||||
require('brace/theme/crimson_editor');
|
||||
|
||||
require('../stylesheets/sql.css');
|
||||
|
||||
$(document).ready(function () {
|
||||
function getParam(name) {
|
||||
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
|
||||
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
|
||||
results = regex.exec(location.search);
|
||||
return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
function initSqlEditorView() {
|
||||
var database_id = $('#database_id').val();
|
||||
var editor = ace.edit("sql");
|
||||
editor.$blockScrolling = Infinity;
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
|
||||
$('#sql').hide();
|
||||
editor.setTheme("ace/theme/crimson_editor");
|
||||
editor.setOptions({
|
||||
minLines: 16,
|
||||
maxLines: Infinity
|
||||
});
|
||||
editor.getSession().setMode("ace/mode/sql");
|
||||
editor.focus();
|
||||
$("select").select2({
|
||||
dropdownAutoWidth: true
|
||||
});
|
||||
|
||||
function showTableMetadata() {
|
||||
$(".metadata").load(
|
||||
'/caravel/table/' + database_id + '/' + $("#dbtable").val() + '/');
|
||||
}
|
||||
$("#dbtable").on("change", showTableMetadata);
|
||||
showTableMetadata();
|
||||
$("#create_view").click(function () {
|
||||
showModal({
|
||||
title: "Error",
|
||||
body: "Sorry, this feature is not yet implemented"
|
||||
});
|
||||
});
|
||||
$(".sqlcontent").show();
|
||||
|
||||
function selectStarOnClick() {
|
||||
$.ajax('/caravel/select_star/' + database_id + '/' + $("#dbtable").val() + '/')
|
||||
.done(function (msg) {
|
||||
editor.setValue(msg);
|
||||
});
|
||||
}
|
||||
|
||||
$("#select_star").click(selectStarOnClick);
|
||||
|
||||
editor.setValue(getParam('sql'));
|
||||
$(window).bind("popstate", function (event) {
|
||||
// Could do something more lightweight here, but we're not optimizing
|
||||
// for the use of the back button anyways
|
||||
editor.setValue(getParam('sql'));
|
||||
$("#run").click();
|
||||
});
|
||||
$("#run").click(function () {
|
||||
$('#results').hide(0);
|
||||
$('#loading').show(0);
|
||||
history.pushState({}, document.title, '?sql=' + encodeURIComponent(editor.getValue()));
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '/caravel/runsql/',
|
||||
data: {
|
||||
data: JSON.stringify({
|
||||
database_id: $('#database_id').val(),
|
||||
sql: editor.getSession().getValue()
|
||||
})
|
||||
},
|
||||
success: function (data) {
|
||||
$('#loading').hide(0);
|
||||
$('#results').show(0);
|
||||
$('#results').html(data);
|
||||
|
||||
$('table.sql_results').DataTable({
|
||||
paging: false,
|
||||
searching: true,
|
||||
aaSorting: []
|
||||
});
|
||||
},
|
||||
error: function (err, err2) {
|
||||
$('#loading').hide(0);
|
||||
$('#results').show(0);
|
||||
$('#results').html(err.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
initSqlEditorView();
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var px = require('./modules/caravel.js');
|
||||
|
||||
require('bootstrap');
|
||||
|
||||
$(document).ready(function () {
|
||||
var slice;
|
||||
var data = $('.slice').data('slice');
|
||||
slice = px.Slice(data);
|
||||
slice.render();
|
||||
slice.bindResizeToWindowResize();
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
|
||||
require('../stylesheets/welcome.css');
|
||||
require('bootstrap');
|
||||
require('datatables.net-bs');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
require('../node_modules/cal-heatmap/cal-heatmap.css');
|
||||
|
||||
var CalHeatMap = require('cal-heatmap');
|
||||
|
||||
function modelViewTable(selector, modelView, orderCol, order) {
|
||||
// Builds a dataTable from a flask appbuilder api endpoint
|
||||
var url = '/' + modelView.toLowerCase() + '/api/read';
|
||||
url += '?_oc_' + modelView + '=' + orderCol;
|
||||
url += '&_od_' + modelView +'=' + order;
|
||||
$.getJSON(url, function (data) {
|
||||
var tableData = jQuery.map(data.result, function (el, i) {
|
||||
var row = $.map(data.list_columns, function (col, i) {
|
||||
return el[col];
|
||||
});
|
||||
return [row];
|
||||
});
|
||||
var cols = jQuery.map(data.list_columns, function (col, i) {
|
||||
return { sTitle: data.label_columns[col] };
|
||||
});
|
||||
var panel = $(selector).parents('.panel');
|
||||
panel.find("img.loading").remove();
|
||||
$(selector).DataTable({
|
||||
aaData: tableData,
|
||||
aoColumns: cols,
|
||||
bPaginate: true,
|
||||
pageLength: 10,
|
||||
bLengthChange: false,
|
||||
aaSorting: [],
|
||||
searching: true,
|
||||
bInfo: false
|
||||
});
|
||||
|
||||
// Hack to move the searchbox in the right spot
|
||||
var search = panel.find(".dataTables_filter input");
|
||||
search.addClass('form-control').detach();
|
||||
search.appendTo(panel.find(".search"));
|
||||
panel.find('.dataTables_filter').remove();
|
||||
|
||||
// Hack to display the page navigator properly
|
||||
panel.find('.col-sm-5').remove();
|
||||
var nav = panel.find('.col-sm-7');
|
||||
nav.removeClass('col-sm-7');
|
||||
nav.addClass('col-sm-12');
|
||||
|
||||
$(selector).slideDown();
|
||||
$('[data-toggle="tooltip"]').tooltip({ container: 'body' });
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
var cal = new CalHeatMap();
|
||||
cal.init({
|
||||
start: new Date().setFullYear(new Date().getFullYear() - 1),
|
||||
range: 13,
|
||||
data: '/caravel/activity_per_day',
|
||||
domain: "month",
|
||||
subDomain: "day",
|
||||
itemName: "action",
|
||||
tooltip: true
|
||||
});
|
||||
modelViewTable('#dash_table', 'DashboardModelViewAsync', 'changed_on', 'desc');
|
||||
modelViewTable('#slice_table', 'SliceAsync', 'changed_on', 'desc');
|
||||
});
|
||||
@@ -1,78 +0,0 @@
|
||||
{
|
||||
"name": "caravel",
|
||||
"version": "0.1.0",
|
||||
"description": "Any database to any visualization",
|
||||
"directories": {
|
||||
"doc": "docs",
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "webpack -d --watch --colors",
|
||||
"prod": "webpack -p --colors",
|
||||
"lint": "npm run --silent lint:js",
|
||||
"lint:js": "eslint --ignore-path=.eslintignore --ext .js ."
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/airbnb/caravel.git"
|
||||
},
|
||||
"keywords": [
|
||||
"big",
|
||||
"data",
|
||||
"exploratory",
|
||||
"analysis",
|
||||
"react",
|
||||
"d3",
|
||||
"airbnb",
|
||||
"nerds",
|
||||
"database",
|
||||
"flask"
|
||||
],
|
||||
"author": "Airbnb",
|
||||
"bugs": {
|
||||
"url": "https://github.com/airbnb/caravel/issues"
|
||||
},
|
||||
"homepage": "https://github.com/airbnb/caravel#readme",
|
||||
"dependencies": {
|
||||
"babel-loader": "^6.2.1",
|
||||
"babel-polyfill": "^6.3.14",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"bootstrap": "^3.3.6",
|
||||
"bootstrap-datepicker": "^1.6.0",
|
||||
"bootstrap-toggle": "^2.2.1",
|
||||
"brace": "^0.7.0",
|
||||
"cal-heatmap": "3.5.4",
|
||||
"css-loader": "^0.23.1",
|
||||
"d3": "^3.5.14",
|
||||
"d3-cloud": "^1.2.1",
|
||||
"d3-sankey": "^0.2.1",
|
||||
"d3-tip": "^0.6.7",
|
||||
"datamaps": "^0.4.4",
|
||||
"datatables-bootstrap3-plugin": "^0.4.0",
|
||||
"datatables.net-bs": "^1.10.11",
|
||||
"exports-loader": "^0.6.3",
|
||||
"font-awesome": "^4.5.0",
|
||||
"gridster": "^0.5.6",
|
||||
"imports-loader": "^0.6.5",
|
||||
"jquery": "^2.2.1",
|
||||
"jquery-ui": "^1.10.5",
|
||||
"less": "^2.6.1",
|
||||
"less-loader": "^2.2.2",
|
||||
"nvd3": "1.8.2",
|
||||
"react": "^0.14.7",
|
||||
"react-bootstrap": "^0.28.3",
|
||||
"react-dom": "^0.14.7",
|
||||
"select2": "3.5",
|
||||
"select2-bootstrap-css": "^1.4.6",
|
||||
"style-loader": "^0.13.0",
|
||||
"topojson": "^1.6.22",
|
||||
"webpack": "^1.12.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^2.2.0",
|
||||
"file-loader": "^0.8.5",
|
||||
"url-loader": "^0.5.7"
|
||||
}
|
||||
}
|
||||
@@ -1,616 +0,0 @@
|
||||
// Paper 3.3.5
|
||||
// Bootswatch
|
||||
// -----------------------------------------------------
|
||||
|
||||
@web-font-path: "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700";
|
||||
|
||||
.web-font(@path) {
|
||||
@import url("@{path}");
|
||||
}
|
||||
.web-font(@web-font-path);
|
||||
|
||||
// Navbar =====================================================================
|
||||
|
||||
.navbar {
|
||||
border: none;
|
||||
.box-shadow(0 1px 2px rgba(0,0,0,.3));
|
||||
|
||||
&-brand {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
&-inverse {
|
||||
.navbar-form {
|
||||
|
||||
input[type=text],
|
||||
input[type=password] {
|
||||
color: #fff;
|
||||
.box-shadow(inset 0 -1px 0 @navbar-inverse-link-color);
|
||||
.placeholder(@navbar-inverse-link-color);
|
||||
|
||||
&:focus {
|
||||
.box-shadow(inset 0 -2px 0 #fff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons ====================================================================
|
||||
|
||||
#btn(@class,@bg) {
|
||||
.btn-@{class} {
|
||||
background-size: 200%;
|
||||
background-position: 50%;
|
||||
|
||||
&:focus {
|
||||
background-color: @bg;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:active:hover {
|
||||
background-color: darken(@bg, 6%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: darken(@bg, 12%);
|
||||
#gradient > .radial(darken(@bg, 12%) 10%, @bg 11%);
|
||||
background-size: 1000%;
|
||||
.box-shadow(2px 2px 4px rgba(0,0,0,.4));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#btn(default,@btn-default-bg);
|
||||
#btn(primary,@btn-primary-bg);
|
||||
#btn(success,@btn-success-bg);
|
||||
#btn(info,@btn-info-bg);
|
||||
#btn(warning,@btn-warning-bg);
|
||||
#btn(danger,@btn-danger-bg);
|
||||
#btn(link,#fff);
|
||||
|
||||
.btn {
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
.box-shadow(1px 1px 4px rgba(0,0,0,.4));
|
||||
.transition(all 0.4s);
|
||||
|
||||
&-link {
|
||||
border-radius: @btn-border-radius-base;
|
||||
.box-shadow(none);
|
||||
color: @btn-default-color;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.box-shadow(none);
|
||||
color: @btn-default-color;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-default {
|
||||
|
||||
&.disabled {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: rgba(0, 0, 0, 0.4);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
.btn + .btn,
|
||||
.btn + .btn-group,
|
||||
.btn-group + .btn,
|
||||
.btn-group + .btn-group {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&-vertical {
|
||||
> .btn + .btn,
|
||||
> .btn + .btn-group,
|
||||
> .btn-group + .btn,
|
||||
> .btn-group + .btn-group {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Typography =================================================================
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
letter-spacing: .1px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
letter-spacing: .1px;
|
||||
}
|
||||
|
||||
a {
|
||||
.transition(all 0.2s);
|
||||
}
|
||||
|
||||
// Tables =====================================================================
|
||||
|
||||
.table-hover {
|
||||
> tbody > tr,
|
||||
> tbody > tr > th,
|
||||
> tbody > tr > td {
|
||||
.transition(all 0.2s);
|
||||
}
|
||||
}
|
||||
|
||||
// Forms ======================================================================
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
textarea,
|
||||
textarea.form-control,
|
||||
input.form-control,
|
||||
input[type=text],
|
||||
input[type=password],
|
||||
input[type=email],
|
||||
input[type=number],
|
||||
[type=text].form-control,
|
||||
[type=password].form-control,
|
||||
[type=email].form-control,
|
||||
[type=tel].form-control,
|
||||
[contenteditable].form-control {
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
.box-shadow(inset 0 -1px 0 #ddd);
|
||||
font-size: 16px;
|
||||
|
||||
&:focus {
|
||||
.box-shadow(inset 0 -2px 0 @brand-primary);
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&[readonly] {
|
||||
.box-shadow(none);
|
||||
border-bottom: 1px dotted #ddd;
|
||||
}
|
||||
|
||||
&.input {
|
||||
&-sm {
|
||||
font-size: @font-size-small;
|
||||
}
|
||||
|
||||
&-lg {
|
||||
font-size: @font-size-large;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select,
|
||||
select.form-control {
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0\9; // remove padding for < ie9 since default arrow can't be removed
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEVmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmaP/QSjAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=);
|
||||
background-size: 13px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: right center;
|
||||
.box-shadow(inset 0 -1px 0 #ddd);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
|
||||
&::-ms-expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.input {
|
||||
&-sm {
|
||||
font-size: @font-size-small;
|
||||
}
|
||||
|
||||
&-lg {
|
||||
font-size: @font-size-large;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
.box-shadow(inset 0 -2px 0 @brand-primary);
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEUhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISF8S9ewAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=);
|
||||
}
|
||||
|
||||
&[multiple] {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.radio,
|
||||
.radio-inline,
|
||||
.checkbox,
|
||||
.checkbox-inline {
|
||||
label {
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
input[type="checkbox"] {
|
||||
margin-left: -25px;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="radio"],
|
||||
.radio input[type="radio"],
|
||||
.radio-inline input[type="radio"] {
|
||||
position: relative;
|
||||
margin-top: 6px;
|
||||
margin-right: 4px;
|
||||
vertical-align: top;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
.transition(240ms);
|
||||
}
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: -3px;
|
||||
background-color: @brand-primary;
|
||||
.scale(0);
|
||||
}
|
||||
|
||||
&:after {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
border: 2px solid @gray;
|
||||
}
|
||||
|
||||
&:checked:before {
|
||||
.scale(0.5);
|
||||
}
|
||||
|
||||
&:disabled:checked:before {
|
||||
background-color: @gray-light;
|
||||
}
|
||||
|
||||
&:checked:after {
|
||||
border-color: @brand-primary;
|
||||
}
|
||||
|
||||
&:disabled:after,
|
||||
&:disabled:checked:after {
|
||||
border-color: @gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
.checkbox input[type="checkbox"],
|
||||
.checkbox-inline input[type="checkbox"] {
|
||||
position: relative;
|
||||
border: none;
|
||||
margin-bottom: -4px;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:focus:after {
|
||||
border-color: @brand-primary;
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-top: -2px;
|
||||
margin-right: 5px;
|
||||
border: 2px solid @gray;
|
||||
border-radius: 2px;
|
||||
.transition(240ms);
|
||||
}
|
||||
|
||||
&:checked:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 6px;
|
||||
display: table;
|
||||
width: 6px;
|
||||
height: 12px;
|
||||
border: 2px solid #fff;
|
||||
border-top-width: 0;
|
||||
border-left-width: 0;
|
||||
.rotate(45deg);
|
||||
}
|
||||
|
||||
&:checked:after {
|
||||
background-color: @brand-primary;
|
||||
border-color: @brand-primary;
|
||||
}
|
||||
|
||||
&:disabled:after {
|
||||
border-color: @gray-light;
|
||||
}
|
||||
|
||||
&:disabled:checked:after {
|
||||
background-color: @gray-light;
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.has-warning {
|
||||
input:not([type=checkbox]),
|
||||
.form-control,
|
||||
input.form-control[readonly],
|
||||
input[type=text][readonly],
|
||||
[type=text].form-control[readonly],
|
||||
input:not([type=checkbox]):focus,
|
||||
.form-control:focus {
|
||||
border-bottom: none;
|
||||
.box-shadow(inset 0 -2px 0 @brand-warning);
|
||||
}
|
||||
}
|
||||
|
||||
.has-error {
|
||||
input:not([type=checkbox]),
|
||||
.form-control,
|
||||
input.form-control[readonly],
|
||||
input[type=text][readonly],
|
||||
[type=text].form-control[readonly],
|
||||
input:not([type=checkbox]):focus,
|
||||
.form-control:focus {
|
||||
border-bottom: none;
|
||||
.box-shadow(inset 0 -2px 0 @brand-danger);
|
||||
}
|
||||
}
|
||||
|
||||
.has-success {
|
||||
input:not([type=checkbox]),
|
||||
.form-control,
|
||||
input.form-control[readonly],
|
||||
input[type=text][readonly],
|
||||
[type=text].form-control[readonly],
|
||||
input:not([type=checkbox]):focus,
|
||||
.form-control:focus {
|
||||
border-bottom: none;
|
||||
.box-shadow(inset 0 -2px 0 @brand-success);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Bootstrap feedback styles for input addons
|
||||
.input-group-addon {
|
||||
.has-warning &, .has-error &, .has-success & {
|
||||
color: @input-color;
|
||||
border-color: @input-group-addon-border-color;
|
||||
background-color: @input-group-addon-bg;
|
||||
}
|
||||
}
|
||||
|
||||
// Navs =======================================================================
|
||||
|
||||
.nav-tabs {
|
||||
> li > a,
|
||||
> li > a:focus {
|
||||
margin-right: 0;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: @navbar-default-link-color;
|
||||
.box-shadow(inset 0 -1px 0 #ddd);
|
||||
.transition(all 0.2s);
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
.box-shadow(inset 0 -2px 0 @brand-primary);
|
||||
color: @brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
& > li.active > a,
|
||||
& > li.active > a:focus {
|
||||
border: none;
|
||||
.box-shadow(inset 0 -2px 0 @brand-primary);
|
||||
color: @brand-primary;
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
color: @brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
& > li.disabled > a {
|
||||
.box-shadow(inset 0 -1px 0 #ddd);
|
||||
}
|
||||
|
||||
&.nav-justified {
|
||||
|
||||
& > li > a,
|
||||
& > li > a:hover,
|
||||
& > li > a:focus,
|
||||
& > .active > a,
|
||||
& > .active > a:hover,
|
||||
& > .active > a:focus {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
margin-top: 0;
|
||||
border: none;
|
||||
.box-shadow(0 1px 4px rgba(0,0,0,.3));
|
||||
}
|
||||
|
||||
// Indicators =================================================================
|
||||
|
||||
.alert {
|
||||
border: none;
|
||||
color: #fff;
|
||||
|
||||
&-success {
|
||||
background-color: @brand-success;
|
||||
}
|
||||
|
||||
&-info {
|
||||
background-color: @brand-info;
|
||||
}
|
||||
|
||||
&-warning {
|
||||
background-color: @brand-warning;
|
||||
}
|
||||
|
||||
&-danger {
|
||||
background-color: @brand-danger;
|
||||
}
|
||||
|
||||
a:not(.close),
|
||||
.alert-link {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 6px 4px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
height: 6px;
|
||||
border-radius: 0;
|
||||
|
||||
.box-shadow(none);
|
||||
|
||||
&-bar {
|
||||
.box-shadow(none);
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&:before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: -1;
|
||||
background-color: lighten(@progress-bar-bg, 35%);
|
||||
}
|
||||
}
|
||||
|
||||
&-success:last-child.progress-bar:before {
|
||||
background-color: lighten(@brand-success, 35%);
|
||||
}
|
||||
|
||||
&-info:last-child.progress-bar:before {
|
||||
background-color: lighten(@brand-info, 45%);
|
||||
}
|
||||
&-warning:last-child.progress-bar:before {
|
||||
background-color: lighten(@brand-warning, 35%);
|
||||
}
|
||||
|
||||
&-danger:last-child.progress-bar:before {
|
||||
background-color: lighten(@brand-danger, 25%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Progress bars ==============================================================
|
||||
|
||||
// Containers =================================================================
|
||||
|
||||
.close {
|
||||
font-size: 34px;
|
||||
font-weight: 300;
|
||||
line-height: 24px;
|
||||
opacity: 0.6;
|
||||
.transition(all 0.2s);
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-group {
|
||||
|
||||
&-item {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
&-item-text {
|
||||
color: @gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
.well {
|
||||
border-radius: 0;
|
||||
.box-shadow(none);
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
.box-shadow(0 1px 4px rgba(0,0,0,.3));
|
||||
|
||||
&-heading {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
border: none;
|
||||
.box-shadow(0 1px 4px rgba(0,0,0,.3));
|
||||
}
|
||||
|
||||
.carousel {
|
||||
&-caption {
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,183 +0,0 @@
|
||||
// JS
|
||||
var d3 = window.d3 || require('d3');
|
||||
|
||||
// CSS
|
||||
require('./big_number.css');
|
||||
|
||||
var px = require('../javascripts/modules/caravel.js');
|
||||
|
||||
function bigNumberVis(slice) {
|
||||
var div = d3.select(slice.selector);
|
||||
|
||||
function render() {
|
||||
d3.json(slice.jsonEndpoint(), function (error, payload) {
|
||||
//Define the percentage bounds that define color from red to green
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var fd = payload.form_data;
|
||||
var json = payload.data;
|
||||
var color_range = [-1, 1];
|
||||
|
||||
var f = d3.format(fd.y_axis_format);
|
||||
var fp = d3.format('+.1%');
|
||||
var width = slice.width();
|
||||
var height = slice.height();
|
||||
div.selectAll("*").remove();
|
||||
var svg = div.append('svg');
|
||||
svg.attr("width", width);
|
||||
svg.attr("height", height);
|
||||
var data = json.data;
|
||||
var compare_suffix = ' ' + json.compare_suffix;
|
||||
var v_compare = null;
|
||||
var v = null;
|
||||
if (fd.viz_type === 'big_number') {
|
||||
v = data[data.length - 1][1];
|
||||
} else {
|
||||
v = data[0][0];
|
||||
}
|
||||
if (json.compare_lag > 0) {
|
||||
var pos = data.length - (json.compare_lag + 1);
|
||||
if (pos >= 0) {
|
||||
v_compare = (v / data[pos][1]) - 1;
|
||||
}
|
||||
}
|
||||
var date_ext = d3.extent(data, function (d) {
|
||||
return d[0];
|
||||
});
|
||||
var value_ext = d3.extent(data, function (d) {
|
||||
return d[1];
|
||||
});
|
||||
|
||||
var margin = 20;
|
||||
var scale_x = d3.time.scale.utc().domain(date_ext).range([margin, width - margin]);
|
||||
var scale_y = d3.scale.linear().domain(value_ext).range([height - (margin), margin]);
|
||||
var colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
|
||||
var scale_color = d3.scale
|
||||
.linear().domain(color_range)
|
||||
.interpolate(d3.interpolateHsl)
|
||||
.range(colorRange).clamp(true);
|
||||
var line = d3.svg.line()
|
||||
.x(function (d) {
|
||||
return scale_x(d[0]);
|
||||
})
|
||||
.y(function (d) {
|
||||
return scale_y(d[1]);
|
||||
})
|
||||
.interpolate("basis");
|
||||
|
||||
var g = svg.append('g');
|
||||
var y = height / 2;
|
||||
//Printing big number
|
||||
g.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', y)
|
||||
.attr('class', 'big')
|
||||
.attr('alignment-baseline', 'middle')
|
||||
.attr('id', 'bigNumber')
|
||||
.style('font-weight', 'bold')
|
||||
.style('cursor', 'pointer')
|
||||
.text(f(v))
|
||||
.style('font-size', d3.min([height, width]) / 3.5)
|
||||
.attr('fill', 'white');
|
||||
|
||||
if (fd.viz_type === 'big_number') {
|
||||
//Drawing trend line
|
||||
|
||||
g.append('path')
|
||||
.attr('d', function (d) {
|
||||
return line(data);
|
||||
})
|
||||
.attr('stroke-width', 5)
|
||||
.attr('opacity', 0.5)
|
||||
.attr('fill', "none")
|
||||
.attr('stroke-linecap', "round")
|
||||
.attr('stroke', "grey");
|
||||
|
||||
g = svg.append('g')
|
||||
.attr('class', 'digits')
|
||||
.attr('opacity', 1);
|
||||
|
||||
if (v_compare !== null) {
|
||||
y = (height / 8) * 3;
|
||||
}
|
||||
|
||||
//Printing big number subheader text
|
||||
if (json.subheader !== null) {
|
||||
g.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', y + d3.min([height, width]) / 4.5)
|
||||
.text(json.subheader)
|
||||
.attr('id', 'subheader_text')
|
||||
.style('font-size', d3.min([height, width]) / 16)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('fill', c)
|
||||
.attr('stroke', c);
|
||||
}
|
||||
|
||||
var c = scale_color(v_compare);
|
||||
|
||||
//Printing compare %
|
||||
if (v_compare !== null) {
|
||||
g.append('text')
|
||||
.attr('x', width / 2)
|
||||
.attr('y', (height / 16) * 12)
|
||||
.text(fp(v_compare) + compare_suffix)
|
||||
.style('font-size', d3.min([height, width]) / 8)
|
||||
.style('text-anchor', 'middle')
|
||||
.attr('fill', c)
|
||||
.attr('stroke', c);
|
||||
}
|
||||
|
||||
var g_axis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
|
||||
g = g_axis.append('g');
|
||||
var x_axis = d3.svg.axis()
|
||||
.scale(scale_x)
|
||||
.orient('bottom')
|
||||
.ticks(4)
|
||||
.tickFormat(px.formatDate);
|
||||
g.call(x_axis);
|
||||
g.attr('transform', 'translate(0,' + (height - margin) + ')');
|
||||
|
||||
g = g_axis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
|
||||
var y_axis = d3.svg.axis()
|
||||
.scale(scale_y)
|
||||
.orient('left')
|
||||
.tickFormat(d3.format(fd.y_axis_format))
|
||||
.tickValues(value_ext);
|
||||
g.call(y_axis);
|
||||
g.selectAll('text')
|
||||
.style('text-anchor', 'end')
|
||||
.attr('y', '-7')
|
||||
.attr('x', '-4');
|
||||
|
||||
g.selectAll("text")
|
||||
.style('font-size', '10px');
|
||||
|
||||
div.on('mouseover', function (d) {
|
||||
var div = d3.select(this);
|
||||
div.select('path').transition().duration(500).attr('opacity', 1)
|
||||
.style('stroke-width', '2px');
|
||||
div.select('g.digits').transition().duration(500).attr('opacity', 0.1);
|
||||
div.select('g.axis').transition().duration(500).attr('opacity', 1);
|
||||
})
|
||||
.on('mouseout', function (d) {
|
||||
var div = d3.select(this);
|
||||
div.select('path').transition().duration(500).attr('opacity', 0.5)
|
||||
.style('stroke-width', '5px');
|
||||
div.select('g.digits').transition().duration(500).attr('opacity', 1);
|
||||
div.select('g.axis').transition().duration(500).attr('opacity', 0);
|
||||
});
|
||||
}
|
||||
slice.done(payload);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: render,
|
||||
resize: render
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = bigNumberVis;
|
||||
@@ -1,175 +0,0 @@
|
||||
// JS
|
||||
var d3 = window.d3 || require('d3');
|
||||
|
||||
// CSS
|
||||
require('./directed_force.css');
|
||||
|
||||
/* Modified from http://bl.ocks.org/d3noob/5141278 */
|
||||
function directedForceVis(slice) {
|
||||
var div = d3.select(slice.selector);
|
||||
var link_length = slice.data.form_data.link_length || 200;
|
||||
var charge = slice.data.form_data.charge || -500;
|
||||
|
||||
var render = function () {
|
||||
var width = slice.width();
|
||||
var height = slice.height() - 25;
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var links = json.data;
|
||||
var nodes = {};
|
||||
// Compute the distinct nodes from the links.
|
||||
links.forEach(function (link) {
|
||||
link.source = nodes[link.source] || (nodes[link.source] = {
|
||||
name: link.source
|
||||
});
|
||||
link.target = nodes[link.target] || (nodes[link.target] = {
|
||||
name: link.target
|
||||
});
|
||||
link.value = Number(link.value);
|
||||
|
||||
var target_name = link.target.name;
|
||||
var source_name = link.source.name;
|
||||
|
||||
if (nodes[target_name].total === undefined) {
|
||||
nodes[target_name].total = link.value;
|
||||
}
|
||||
if (nodes[source_name].total === undefined) {
|
||||
nodes[source_name].total = 0;
|
||||
}
|
||||
if (nodes[target_name].max === undefined) {
|
||||
nodes[target_name].max = 0;
|
||||
}
|
||||
if (link.value > nodes[target_name].max) {
|
||||
nodes[target_name].max = link.value;
|
||||
}
|
||||
if (nodes[target_name].min === undefined) {
|
||||
nodes[target_name].min = 0;
|
||||
}
|
||||
if (link.value > nodes[target_name].min) {
|
||||
nodes[target_name].min = link.value;
|
||||
}
|
||||
|
||||
nodes[target_name].total += link.value;
|
||||
});
|
||||
|
||||
var force = d3.layout.force()
|
||||
.nodes(d3.values(nodes))
|
||||
.links(links)
|
||||
.size([width, height])
|
||||
.linkDistance(link_length)
|
||||
.charge(charge)
|
||||
.on("tick", tick)
|
||||
.start();
|
||||
|
||||
var svg = div.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
// build the arrow.
|
||||
svg.append("svg:defs").selectAll("marker")
|
||||
.data(["end"]) // Different link/path types can be defined here
|
||||
.enter().append("svg:marker") // This section adds in the arrows
|
||||
.attr("id", String)
|
||||
.attr("viewBox", "0 -5 10 10")
|
||||
.attr("refX", 15)
|
||||
.attr("refY", -1.5)
|
||||
.attr("markerWidth", 6)
|
||||
.attr("markerHeight", 6)
|
||||
.attr("orient", "auto")
|
||||
.append("svg:path")
|
||||
.attr("d", "M0,-5L10,0L0,5");
|
||||
|
||||
var edgeScale = d3.scale.linear()
|
||||
.range([0.1, 0.5]);
|
||||
// add the links and the arrows
|
||||
var path = svg.append("svg:g").selectAll("path")
|
||||
.data(force.links())
|
||||
.enter().append("svg:path")
|
||||
.attr("class", "link")
|
||||
.style("opacity", function (d) {
|
||||
return edgeScale(d.value / d.target.max);
|
||||
})
|
||||
.attr("marker-end", "url(#end)");
|
||||
|
||||
// define the nodes
|
||||
var node = svg.selectAll(".node")
|
||||
.data(force.nodes())
|
||||
.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.on("mouseenter", function (d) {
|
||||
d3.select(this)
|
||||
.select("circle")
|
||||
.transition()
|
||||
.style('stroke-width', 5);
|
||||
|
||||
d3.select(this)
|
||||
.select("text")
|
||||
.transition()
|
||||
.style('font-size', 25);
|
||||
})
|
||||
.on("mouseleave", function (d) {
|
||||
d3.select(this)
|
||||
.select("circle")
|
||||
.transition()
|
||||
.style('stroke-width', 1.5);
|
||||
d3.select(this)
|
||||
.select("text")
|
||||
.transition()
|
||||
.style('font-size', 12);
|
||||
})
|
||||
.call(force.drag);
|
||||
|
||||
// add the nodes
|
||||
var ext = d3.extent(d3.values(nodes), function (d) {
|
||||
return Math.sqrt(d.total);
|
||||
});
|
||||
var circleScale = d3.scale.linear()
|
||||
.domain(ext)
|
||||
.range([3, 30]);
|
||||
|
||||
node.append("circle")
|
||||
.attr("r", function (d) {
|
||||
return circleScale(Math.sqrt(d.total));
|
||||
});
|
||||
|
||||
// add the text
|
||||
node.append("text")
|
||||
.attr("x", 6)
|
||||
.attr("dy", ".35em")
|
||||
.text(function (d) {
|
||||
return d.name;
|
||||
});
|
||||
|
||||
// add the curvy lines
|
||||
function tick() {
|
||||
path.attr("d", function (d) {
|
||||
var dx = d.target.x - d.source.x,
|
||||
dy = d.target.y - d.source.y,
|
||||
dr = Math.sqrt(dx * dx + dy * dy);
|
||||
return "M" +
|
||||
d.source.x + "," +
|
||||
d.source.y + "A" +
|
||||
dr + "," + dr + " 0 0,1 " +
|
||||
d.target.x + "," +
|
||||
d.target.y;
|
||||
});
|
||||
|
||||
node.attr("transform", function (d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
});
|
||||
}
|
||||
|
||||
slice.done(json);
|
||||
});
|
||||
};
|
||||
return {
|
||||
render: render,
|
||||
resize: render
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = directedForceVis;
|
||||
@@ -1,8 +0,0 @@
|
||||
.select2-highlighted > .filter_box {
|
||||
background-color: transparent;
|
||||
border: 1px caravel black;
|
||||
}
|
||||
|
||||
.dashboard .filter_box .slice_container > div {
|
||||
padding-top: 0;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// JS
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var d3 = window.d3 || require('d3');
|
||||
|
||||
// CSS
|
||||
require('./filter_box.css');
|
||||
require('../javascripts/caravel-select2.js');
|
||||
|
||||
function filterBox(slice) {
|
||||
var filtersObj = {};
|
||||
var d3token = d3.select(slice.selector);
|
||||
|
||||
var fltChanged = function () {
|
||||
var val = $(this).val();
|
||||
var vals = [];
|
||||
if (val !== '') {
|
||||
vals = val.split(',');
|
||||
}
|
||||
slice.setFilter($(this).attr('name'), vals);
|
||||
};
|
||||
|
||||
var refresh = function () {
|
||||
d3token.selectAll("*").remove();
|
||||
var container = d3token
|
||||
.append('div')
|
||||
.classed('padded', true);
|
||||
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
var maxes = {};
|
||||
|
||||
for (var filter in payload.data) {
|
||||
var data = payload.data[filter];
|
||||
maxes[filter] = d3.max(data, function (d) {
|
||||
return d.metric;
|
||||
});
|
||||
var id = 'fltbox__' + filter;
|
||||
|
||||
var div = container.append('div');
|
||||
|
||||
div.append("label").text(filter);
|
||||
|
||||
div.append('div')
|
||||
.attr('name', filter)
|
||||
.classed('form-control', true)
|
||||
.attr('multiple', '')
|
||||
.attr('id', id);
|
||||
|
||||
filtersObj[filter] = $('#' + id).select2({
|
||||
placeholder: "Select [" + filter + ']',
|
||||
containment: 'parent',
|
||||
dropdownAutoWidth: true,
|
||||
data: data,
|
||||
multiple: true,
|
||||
formatResult: select2Formatter
|
||||
})
|
||||
.on('change', fltChanged);
|
||||
}
|
||||
slice.done(payload);
|
||||
|
||||
function select2Formatter(result, container /*, query, escapeMarkup*/) {
|
||||
var perc = Math.round((result.metric / maxes[result.filter]) * 100);
|
||||
var style = 'padding: 2px 5px;';
|
||||
style += "background-image: ";
|
||||
style += "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
|
||||
|
||||
$(container).attr('style', 'padding: 0px; background: white;');
|
||||
$(container).addClass('filter_box');
|
||||
return '<div style="' + style + '"><span>' + result.text + '</span></div>';
|
||||
}
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
});
|
||||
};
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = filterBox;
|
||||
@@ -1,234 +0,0 @@
|
||||
// JS
|
||||
var $ = window.$ || require('jquery');
|
||||
var px = window.px || require('../javascripts/modules/caravel.js');
|
||||
var d3 = require('d3');
|
||||
|
||||
d3.tip = require('d3-tip'); //using window.d3 doesn't capture events properly bc of multiple instances
|
||||
|
||||
// CSS
|
||||
require('./heatmap.css');
|
||||
|
||||
// Inspired from http://bl.ocks.org/mbostock/3074470
|
||||
// https://jsfiddle.net/cyril123/h0reyumq/
|
||||
function heatmapVis(slice) {
|
||||
|
||||
function refresh() {
|
||||
var margin = {
|
||||
top: 10,
|
||||
right: 10,
|
||||
bottom: 35,
|
||||
left: 35
|
||||
};
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, payload) {
|
||||
var matrix = {};
|
||||
if (error) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var fd = payload.form_data;
|
||||
var data = payload.data;
|
||||
|
||||
// Dynamically adjusts based on max x / y category lengths
|
||||
function adjustMargins(data, margins) {
|
||||
var pixelsPerCharX = 4.5; // approx, depends on font size
|
||||
var pixelsPerCharY = 6.8; // approx, depends on font size
|
||||
var longestX = 1;
|
||||
var longestY = 1;
|
||||
var datum;
|
||||
|
||||
for (var i = 0; i < data.length; i++) {
|
||||
datum = data[i];
|
||||
longestX = Math.max(longestX, datum.x.length || 1);
|
||||
longestY = Math.max(longestY, datum.y.length || 1);
|
||||
}
|
||||
|
||||
margins.left = Math.ceil(Math.max(margins.left, pixelsPerCharY * longestY));
|
||||
margins.bottom = Math.ceil(Math.max(margins.bottom, pixelsPerCharX * longestX));
|
||||
}
|
||||
|
||||
function ordScale(k, rangeBands, reverse) {
|
||||
if (reverse === undefined) {
|
||||
reverse = false;
|
||||
}
|
||||
var domain = {};
|
||||
$.each(data, function (i, d) {
|
||||
domain[d[k]] = true;
|
||||
});
|
||||
domain = Object.keys(domain).sort(function (a, b) {
|
||||
return b - a;
|
||||
});
|
||||
if (reverse) {
|
||||
domain.reverse();
|
||||
}
|
||||
if (rangeBands === undefined) {
|
||||
return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
|
||||
} else {
|
||||
return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
|
||||
}
|
||||
}
|
||||
adjustMargins(data, margin);
|
||||
|
||||
var width = slice.width();
|
||||
var height = slice.height();
|
||||
var hmWidth = width - (margin.left + margin.right);
|
||||
var hmHeight = height - (margin.bottom + margin.top);
|
||||
var fp = d3.format('.3p');
|
||||
|
||||
var xScale = ordScale('x');
|
||||
var yScale = ordScale('y', undefined, true);
|
||||
var xRbScale = ordScale('x', [0, hmWidth]);
|
||||
var yRbScale = ordScale('y', [hmHeight, 0]);
|
||||
var X = 0,
|
||||
Y = 1;
|
||||
var heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
|
||||
|
||||
var color = px.color.colorScalerFactory(fd.linear_color_scheme);
|
||||
|
||||
var scale = [
|
||||
d3.scale.linear()
|
||||
.domain([0, heatmapDim[X]])
|
||||
.range([0, hmWidth]),
|
||||
d3.scale.linear()
|
||||
.domain([0, heatmapDim[Y]])
|
||||
.range([0, hmHeight])
|
||||
];
|
||||
|
||||
var container = d3.select(slice.selector);
|
||||
|
||||
var canvas = container.append("canvas")
|
||||
.attr("width", heatmapDim[X])
|
||||
.attr("height", heatmapDim[Y])
|
||||
.style("width", hmWidth + "px")
|
||||
.style("height", hmHeight + "px")
|
||||
.style("image-rendering", fd.canvas_image_rendering)
|
||||
.style("left", margin.left + "px")
|
||||
.style("top", margin.top + "px")
|
||||
.style("position", "absolute");
|
||||
|
||||
var svg = container.append("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.style("left", "0px")
|
||||
.style("top", "0px")
|
||||
.style("position", "absolute");
|
||||
|
||||
var rect = svg.append('g')
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
.append('rect')
|
||||
.style('fill-opacity', 0)
|
||||
.attr('stroke', 'black')
|
||||
.attr("width", hmWidth)
|
||||
.attr("height", hmHeight);
|
||||
|
||||
var tip = d3.tip()
|
||||
.attr('class', 'd3-tip')
|
||||
.offset(function () {
|
||||
var k = d3.mouse(this);
|
||||
var x = k[0] - (hmWidth / 2);
|
||||
return [k[1] - 20, x];
|
||||
})
|
||||
.html(function (d) {
|
||||
var s = "";
|
||||
var k = d3.mouse(this);
|
||||
var m = Math.floor(scale[0].invert(k[0]));
|
||||
var n = Math.floor(scale[1].invert(k[1]));
|
||||
if (m in matrix && n in matrix[m]) {
|
||||
var obj = matrix[m][n];
|
||||
s += "<div><b>" + fd.all_columns_x + ": </b>" + obj.x + "<div>";
|
||||
s += "<div><b>" + fd.all_columns_y + ": </b>" + obj.y + "<div>";
|
||||
s += "<div><b>" + fd.metric + ": </b>" + obj.v + "<div>";
|
||||
s += "<div><b>%: </b>" + fp(obj.perc) + "<div>";
|
||||
tip.style("display", null);
|
||||
} else {
|
||||
// this is a hack to hide the tooltip because we have map it to a single <rect>
|
||||
// d3-tip toggles opacity and calling hide here is undone by the lib after this call
|
||||
tip.style("display", "none");
|
||||
}
|
||||
return s;
|
||||
});
|
||||
|
||||
rect.call(tip);
|
||||
|
||||
var xAxis = d3.svg.axis()
|
||||
.scale(xRbScale)
|
||||
.tickValues(xRbScale.domain().filter(
|
||||
function (d, i) {
|
||||
return !(i % (parseInt(fd.xscale_interval, 10)));
|
||||
}))
|
||||
.orient("bottom");
|
||||
|
||||
var yAxis = d3.svg.axis()
|
||||
.scale(yRbScale)
|
||||
.tickValues(yRbScale.domain().filter(
|
||||
function (d, i) {
|
||||
return !(i % (parseInt(fd.yscale_interval, 10)));
|
||||
}))
|
||||
.orient("left");
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "x axis")
|
||||
.attr("transform", "translate(" + margin.left + "," + (margin.top + hmHeight) + ")")
|
||||
.call(xAxis)
|
||||
.selectAll("text")
|
||||
.style("text-anchor", "end")
|
||||
.attr("transform", "rotate(-45)");
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "y axis")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
|
||||
.call(yAxis);
|
||||
|
||||
rect.on('mousemove', tip.show);
|
||||
rect.on('mouseout', tip.hide);
|
||||
|
||||
var context = canvas.node().getContext("2d");
|
||||
context.imageSmoothingEnabled = false;
|
||||
createImageObj();
|
||||
|
||||
// Compute the pixel colors; scaled by CSS.
|
||||
function createImageObj() {
|
||||
var imageObj = new Image();
|
||||
var image = context.createImageData(heatmapDim[0], heatmapDim[1]);
|
||||
var pixs = {};
|
||||
$.each(data, function (i, d) {
|
||||
var c = d3.rgb(color(d.perc));
|
||||
var x = xScale(d.x);
|
||||
var y = yScale(d.y);
|
||||
pixs[x + (y * xScale.domain().length)] = c;
|
||||
if (matrix[x] === undefined) {
|
||||
matrix[x] = {};
|
||||
}
|
||||
if (matrix[x][y] === undefined) {
|
||||
matrix[x][y] = d;
|
||||
}
|
||||
});
|
||||
|
||||
var p = -1;
|
||||
for (var i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
|
||||
var c = pixs[i];
|
||||
var alpha = 255;
|
||||
if (c === undefined) {
|
||||
c = d3.rgb('#F00');
|
||||
alpha = 0;
|
||||
}
|
||||
image.data[++p] = c.r;
|
||||
image.data[++p] = c.g;
|
||||
image.data[++p] = c.b;
|
||||
image.data[++p] = alpha;
|
||||
}
|
||||
context.putImageData(image, 0, 0);
|
||||
imageObj.src = canvas.node().toDataURL();
|
||||
}
|
||||
|
||||
slice.done();
|
||||
|
||||
});
|
||||
}
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = heatmapVis;
|
||||
@@ -1,25 +0,0 @@
|
||||
var $ = window.$ || require('jquery');
|
||||
|
||||
function iframeWidget(slice) {
|
||||
|
||||
function refresh() {
|
||||
$('#code').attr('rows', '15');
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
slice.container.html('<iframe style="width:100%;"></iframe>');
|
||||
var iframe = slice.container.find('iframe');
|
||||
iframe.css('height', slice.height());
|
||||
iframe.attr('src', payload.form_data.url);
|
||||
slice.done();
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = iframeWidget;
|
||||
@@ -1,8 +0,0 @@
|
||||
g.caravel path {
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
|
||||
.nvtooltip tr.highlight td {
|
||||
font-weight: bold;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
@@ -1,229 +0,0 @@
|
||||
// JS
|
||||
var $ = window.$ || require('jquery');
|
||||
var d3 = window.d3 || require('d3');
|
||||
var px = window.px || require('../javascripts/modules/caravel.js');
|
||||
var nv = require('nvd3');
|
||||
|
||||
// CSS
|
||||
require('../node_modules/nvd3/build/nv.d3.min.css');
|
||||
require('./nvd3_vis.css');
|
||||
|
||||
function nvd3Vis(slice) {
|
||||
var chart;
|
||||
|
||||
var render = function () {
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
var fd = payload.form_data;
|
||||
var viz_type = fd.viz_type;
|
||||
|
||||
var f = d3.format('.3s');
|
||||
var colorKey = 'key';
|
||||
|
||||
nv.addGraph(function () {
|
||||
switch (viz_type) {
|
||||
case 'line':
|
||||
if (fd.show_brush) {
|
||||
chart = nv.models.lineWithFocusChart();
|
||||
chart.lines2.xScale(d3.time.scale.utc());
|
||||
chart.x2Axis
|
||||
.showMaxMin(fd.x_axis_showminmax)
|
||||
.staggerLabels(false);
|
||||
} else {
|
||||
chart = nv.models.lineChart();
|
||||
}
|
||||
// To alter the tooltip header
|
||||
// chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
|
||||
chart.xScale(d3.time.scale.utc());
|
||||
chart.interpolate(fd.line_interpolation);
|
||||
chart.xAxis
|
||||
.showMaxMin(fd.x_axis_showminmax)
|
||||
.staggerLabels(false);
|
||||
break;
|
||||
|
||||
case 'bar':
|
||||
chart = nv.models.multiBarChart()
|
||||
.showControls(true)
|
||||
.groupSpacing(0.1);
|
||||
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.staggerLabels(true);
|
||||
|
||||
chart.stacked(fd.bar_stacked);
|
||||
break;
|
||||
|
||||
case 'dist_bar':
|
||||
chart = nv.models.multiBarChart()
|
||||
.showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode.
|
||||
.reduceXTicks(false)
|
||||
.rotateLabels(45)
|
||||
.groupSpacing(0.1); //Distance between each group of bars.
|
||||
|
||||
chart.xAxis
|
||||
.showMaxMin(false);
|
||||
|
||||
chart.stacked(fd.bar_stacked);
|
||||
break;
|
||||
|
||||
case 'pie':
|
||||
chart = nv.models.pieChart();
|
||||
colorKey = 'x';
|
||||
chart.valueFormat(f);
|
||||
if (fd.donut) {
|
||||
chart.donut(true);
|
||||
chart.labelsOutside(true);
|
||||
}
|
||||
chart.labelsOutside(true);
|
||||
chart.cornerRadius(true);
|
||||
break;
|
||||
|
||||
case 'column':
|
||||
chart = nv.models.multiBarChart()
|
||||
.reduceXTicks(false)
|
||||
.rotateLabels(45);
|
||||
break;
|
||||
|
||||
case 'compare':
|
||||
chart = nv.models.cumulativeLineChart();
|
||||
chart.xScale(d3.time.scale.utc());
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.staggerLabels(true);
|
||||
break;
|
||||
|
||||
case 'bubble':
|
||||
var row = function (col1, col2) {
|
||||
return "<tr><td>" + col1 + "</td><td>" + col2 + "</td></tr>";
|
||||
};
|
||||
chart = nv.models.scatterChart();
|
||||
chart.showDistX(true);
|
||||
chart.showDistY(true);
|
||||
chart.tooltip.contentGenerator(function (obj) {
|
||||
var p = obj.point;
|
||||
var s = "<table>";
|
||||
s += '<tr><td style="color:' + p.color + ';"><strong>' + p[fd.entity] + '</strong> (' + p.group + ')</td></tr>';
|
||||
s += row(fd.x, f(p.x));
|
||||
s += row(fd.y, f(p.y));
|
||||
s += row(fd.size, f(p.size));
|
||||
s += "</table>";
|
||||
return s;
|
||||
});
|
||||
chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
|
||||
break;
|
||||
|
||||
case 'area':
|
||||
chart = nv.models.stackedAreaChart();
|
||||
chart.style(fd.stacked_style);
|
||||
chart.xScale(d3.time.scale.utc());
|
||||
chart.xAxis
|
||||
.showMaxMin(false)
|
||||
.staggerLabels(true);
|
||||
break;
|
||||
|
||||
case 'box_plot':
|
||||
colorKey = 'label';
|
||||
chart = nv.models.boxPlotChart();
|
||||
chart.x(function (d) { return d.label; });
|
||||
chart.staggerLabels(true);
|
||||
chart.maxBoxWidth(75); // prevent boxes from being incredibly wide
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error("Unrecognized visualization for nvd3" + viz_type);
|
||||
}
|
||||
|
||||
if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') {
|
||||
chart.showLegend(fd.show_legend);
|
||||
}
|
||||
|
||||
var height = slice.height();
|
||||
height -= 15; // accounting for the staggered xAxis
|
||||
|
||||
chart.height(height);
|
||||
slice.container.css('height', height + 'px');
|
||||
|
||||
if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) {
|
||||
chart.useInteractiveGuideline(true);
|
||||
}
|
||||
if (fd.y_axis_zero) {
|
||||
chart.forceY([0, 1]);
|
||||
} else if (fd.y_log_scale) {
|
||||
chart.yScale(d3.scale.log());
|
||||
}
|
||||
if (fd.x_log_scale) {
|
||||
chart.xScale(d3.scale.log());
|
||||
}
|
||||
var xAxisFormatter = null;
|
||||
if (viz_type === 'bubble') {
|
||||
xAxisFormatter = d3.format('.3s');
|
||||
} else if (fd.x_axis_format === 'smart_date') {
|
||||
xAxisFormatter = px.formatDate;
|
||||
chart.xAxis.tickFormat(xAxisFormatter);
|
||||
} else if (fd.x_axis_format !== undefined) {
|
||||
xAxisFormatter = px.timeFormatFactory(fd.x_axis_format);
|
||||
chart.xAxis.tickFormat(xAxisFormatter);
|
||||
}
|
||||
|
||||
if (chart.hasOwnProperty("x2Axis")) {
|
||||
chart.x2Axis.tickFormat(xAxisFormatter);
|
||||
height += 30;
|
||||
}
|
||||
|
||||
if (viz_type === 'bubble') {
|
||||
chart.xAxis.tickFormat(d3.format('.3s'));
|
||||
} else if (fd.x_axis_format === 'smart_date') {
|
||||
chart.xAxis.tickFormat(px.formatDate);
|
||||
} else if (fd.x_axis_format !== undefined) {
|
||||
chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format));
|
||||
}
|
||||
if (chart.yAxis !== undefined) {
|
||||
chart.yAxis.tickFormat(d3.format('.3s'));
|
||||
}
|
||||
|
||||
if (fd.contribution || fd.num_period_compare || viz_type === 'compare') {
|
||||
chart.yAxis.tickFormat(d3.format('.3p'));
|
||||
if (chart.y2Axis !== undefined) {
|
||||
chart.y2Axis.tickFormat(d3.format('.3p'));
|
||||
}
|
||||
} else if (fd.y_axis_format) {
|
||||
chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
|
||||
|
||||
if (chart.y2Axis !== undefined) {
|
||||
chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
|
||||
}
|
||||
}
|
||||
|
||||
chart.color(function (d, i) {
|
||||
return px.color.category21(d[colorKey]);
|
||||
});
|
||||
|
||||
d3.select(slice.selector).html('');
|
||||
d3.select(slice.selector).append("svg")
|
||||
.datum(payload.data)
|
||||
.transition().duration(500)
|
||||
.attr('height', height)
|
||||
.call(chart);
|
||||
|
||||
return chart;
|
||||
});
|
||||
|
||||
slice.done(payload);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
});
|
||||
};
|
||||
|
||||
var update = function () {
|
||||
if (chart && chart.update) {
|
||||
chart.update();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
render: render,
|
||||
resize: update
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = nvd3Vis;
|
||||
@@ -1,92 +0,0 @@
|
||||
// JS
|
||||
var $ = window.$ || require('jquery');
|
||||
var d3 = window.d3 || require('d3');
|
||||
d3.parcoords = require('../vendor/parallel_coordinates/d3.parcoords.js');
|
||||
d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js');
|
||||
|
||||
// CSS
|
||||
require('../vendor/parallel_coordinates/d3.parcoords.css');
|
||||
|
||||
function parallelCoordVis(slice) {
|
||||
|
||||
function refresh() {
|
||||
$('#code').attr('rows', '15');
|
||||
$.getJSON(slice.jsonEndpoint(), function (payload) {
|
||||
var data = payload.data;
|
||||
var fd = payload.form_data;
|
||||
var ext = d3.extent(data, function (d) {
|
||||
return d[fd.secondary_metric];
|
||||
});
|
||||
ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
|
||||
var cScale = d3.scale.linear()
|
||||
.domain(ext)
|
||||
.range(['red', 'grey', 'blue'])
|
||||
.interpolate(d3.interpolateLab);
|
||||
|
||||
var color = function (d) {
|
||||
return cScale(d[fd.secondary_metric]);
|
||||
};
|
||||
var container = d3.select(slice.selector);
|
||||
var eff_height = fd.show_datatable ? (slice.height() / 2) : slice.height();
|
||||
|
||||
container.append('div')
|
||||
.attr('id', 'parcoords_' + slice.container_id)
|
||||
.style('height', eff_height + 'px')
|
||||
.classed("parcoords", true);
|
||||
|
||||
var parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
|
||||
.width(slice.width())
|
||||
.color(color)
|
||||
.alpha(0.5)
|
||||
.composite("darken")
|
||||
.height(eff_height)
|
||||
.data(payload.data)
|
||||
.render()
|
||||
.createAxes()
|
||||
.shadows()
|
||||
.reorderable()
|
||||
.brushMode("1D-axes");
|
||||
|
||||
if (fd.show_datatable) {
|
||||
// create data table, row hover highlighting
|
||||
var grid = d3.divgrid();
|
||||
container.append("div")
|
||||
.datum(data.slice(0, 10))
|
||||
.attr('id', "grid")
|
||||
.call(grid)
|
||||
.classed("parcoords", true)
|
||||
.selectAll(".row")
|
||||
.on({
|
||||
mouseover: function (d) {
|
||||
parcoords.highlight([d]);
|
||||
},
|
||||
mouseout: parcoords.unhighlight
|
||||
});
|
||||
// update data table on brush event
|
||||
parcoords.on("brush", function (d) {
|
||||
d3.select("#grid")
|
||||
.datum(d.slice(0, 10))
|
||||
.call(grid)
|
||||
.selectAll(".row")
|
||||
.on({
|
||||
mouseover: function (d) {
|
||||
parcoords.highlight([d]);
|
||||
},
|
||||
mouseout: parcoords.unhighlight
|
||||
});
|
||||
});
|
||||
}
|
||||
slice.done();
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = parallelCoordVis;
|
||||
@@ -1,32 +0,0 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
|
||||
require('datatables.net-bs');
|
||||
require('./pivot_table.css');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
|
||||
module.exports = function (slice) {
|
||||
var container = slice.container;
|
||||
var form_data = slice.data.form_data;
|
||||
|
||||
function refresh() {
|
||||
$.getJSON(slice.jsonEndpoint(), function (json) {
|
||||
container.html(json.data);
|
||||
if (form_data.groupby.length === 1) {
|
||||
var table = container.find('table').DataTable({
|
||||
paging: false,
|
||||
searching: false,
|
||||
bInfo: false
|
||||
});
|
||||
table.column('-1').order('desc').draw();
|
||||
}
|
||||
slice.done(json);
|
||||
}).fail(function (xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
});
|
||||
}
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh
|
||||
};
|
||||
};
|
||||
@@ -1,177 +0,0 @@
|
||||
// CSS
|
||||
require('./sankey.css');
|
||||
// JS
|
||||
var px = window.px || require('../javascripts/modules/caravel.js');
|
||||
var d3 = window.d3 || require('d3');
|
||||
d3.sankey = require('d3-sankey').sankey;
|
||||
|
||||
function sankeyVis(slice) {
|
||||
var div = d3.select(slice.selector);
|
||||
|
||||
var render = function () {
|
||||
var margin = {
|
||||
top: 5,
|
||||
right: 5,
|
||||
bottom: 5,
|
||||
left: 5
|
||||
};
|
||||
var width = slice.width() - margin.left - margin.right;
|
||||
var height = slice.height() - margin.top - margin.bottom;
|
||||
|
||||
var formatNumber = d3.format(",.2f");
|
||||
|
||||
div.selectAll("*").remove();
|
||||
var svg = div.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
var tooltip = div.append("div")
|
||||
.attr("class", "sankey-tooltip")
|
||||
.style("opacity", 0);
|
||||
|
||||
var sankey = d3.sankey()
|
||||
.nodeWidth(15)
|
||||
.nodePadding(10)
|
||||
.size([width, height]);
|
||||
|
||||
var path = sankey.link();
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var links = json.data;
|
||||
var nodes = {};
|
||||
// Compute the distinct nodes from the links.
|
||||
links.forEach(function (link) {
|
||||
link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
|
||||
link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
|
||||
link.value = Number(link.value);
|
||||
});
|
||||
nodes = d3.values(nodes);
|
||||
|
||||
sankey
|
||||
.nodes(nodes)
|
||||
.links(links)
|
||||
.layout(32);
|
||||
|
||||
var link = svg.append("g").selectAll(".link")
|
||||
.data(links)
|
||||
.enter().append("path")
|
||||
.attr("class", "link")
|
||||
.attr("d", path)
|
||||
.style("stroke-width", function (d) {
|
||||
return Math.max(1, d.dy);
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return b.dy - a.dy;
|
||||
})
|
||||
.on("mouseover", onmouseover)
|
||||
.on("mouseout", onmouseout);
|
||||
|
||||
var node = svg.append("g").selectAll(".node")
|
||||
.data(nodes)
|
||||
.enter().append("g")
|
||||
.attr("class", "node")
|
||||
.attr("transform", function (d) {
|
||||
return "translate(" + d.x + "," + d.y + ")";
|
||||
})
|
||||
.call(d3.behavior.drag()
|
||||
.origin(function (d) {
|
||||
return d;
|
||||
})
|
||||
.on("dragstart", function () {
|
||||
this.parentNode.appendChild(this);
|
||||
})
|
||||
.on("drag", dragmove));
|
||||
|
||||
node.append("rect")
|
||||
.attr("height", function (d) {
|
||||
return d.dy;
|
||||
})
|
||||
.attr("width", sankey.nodeWidth())
|
||||
.style("fill", function (d) {
|
||||
d.color = px.color.category21(d.name.replace(/ .*/, ""));
|
||||
return d.color;
|
||||
})
|
||||
.style("stroke", function (d) {
|
||||
return d3.rgb(d.color).darker(2);
|
||||
})
|
||||
.on("mouseover", onmouseover)
|
||||
.on("mouseout", onmouseout);
|
||||
|
||||
node.append("text")
|
||||
.attr("x", -6)
|
||||
.attr("y", function (d) {
|
||||
return d.dy / 2;
|
||||
})
|
||||
.attr("dy", ".35em")
|
||||
.attr("text-anchor", "end")
|
||||
.attr("transform", null)
|
||||
.text(function (d) {
|
||||
return d.name;
|
||||
})
|
||||
.filter(function (d) {
|
||||
return d.x < width / 2;
|
||||
})
|
||||
.attr("x", 6 + sankey.nodeWidth())
|
||||
.attr("text-anchor", "start");
|
||||
|
||||
function dragmove(d) {
|
||||
d3.select(this)
|
||||
.attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
|
||||
|
||||
sankey.relayout();
|
||||
link.attr("d", path);
|
||||
}
|
||||
|
||||
function getTooltipHtml(d) {
|
||||
var html;
|
||||
|
||||
if (d.sourceLinks) { // is node
|
||||
html = d.name + " Value: <span class='emph'>" + formatNumber(d.value) + "</span>";
|
||||
} else {
|
||||
var val = formatNumber(d.value);
|
||||
var sourcePercent = d3.round((d.value / d.source.value) * 100, 1);
|
||||
var targetPercent = d3.round((d.value / d.target.value) * 100, 1);
|
||||
|
||||
html = [
|
||||
"<div class=''>Path Value: <span class='emph'>", val, "</span></div>",
|
||||
"<div class='percents'>",
|
||||
"<span class='emph'>", (isFinite(sourcePercent) ? sourcePercent : "100"), "%</span> of ", d.source.name, "<br/>",
|
||||
"<span class='emph'>" + (isFinite(targetPercent) ? targetPercent : "--") + "%</span> of ", d.target.name, "target",
|
||||
"</div>"
|
||||
].join("");
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function onmouseover(d) {
|
||||
tooltip
|
||||
.html(function () { return getTooltipHtml(d); })
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style("left", (d3.event.layerX + 10) + "px")
|
||||
.style("top", (d3.event.layerY + 10) + "px")
|
||||
.style("opacity", 0.95);
|
||||
}
|
||||
|
||||
function onmouseout(d) {
|
||||
tooltip.transition()
|
||||
.duration(100)
|
||||
.style("opacity", 0);
|
||||
}
|
||||
|
||||
slice.done(json);
|
||||
});
|
||||
};
|
||||
return {
|
||||
render: render,
|
||||
resize: render
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = sankeyVis;
|
||||
@@ -1,375 +0,0 @@
|
||||
var d3 = window.d3 || require('d3');
|
||||
var px = require('../javascripts/modules/caravel.js');
|
||||
var wrapSvgText = require('../javascripts/modules/utils.js').wrapSvgText;
|
||||
|
||||
require('./sunburst.css');
|
||||
|
||||
// Modified from http://bl.ocks.org/kerryrodden/7090426
|
||||
function sunburstVis(slice) {
|
||||
var container = d3.select(slice.selector);
|
||||
|
||||
var render = function () {
|
||||
// vars with shared scope within this function
|
||||
var margin = { top: 10, right: 5, bottom: 10, left: 5 };
|
||||
var containerWidth = slice.width();
|
||||
var containerHeight = slice.height();
|
||||
var breadcrumbHeight = containerHeight * 0.085;
|
||||
var visWidth = containerWidth - margin.left - margin.right;
|
||||
var visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
|
||||
var radius = Math.min(visWidth, visHeight) / 2;
|
||||
var colorByCategory = true; // color by category if primary/secondary metrics match
|
||||
|
||||
var maxBreadcrumbs, breadcrumbDims, // set based on data
|
||||
totalSize, // total size of all segments; set after loading the data.
|
||||
colorScale,
|
||||
breadcrumbs, vis, arcs, gMiddleText; // dom handles
|
||||
|
||||
// Helper + path gen functions
|
||||
var partition = d3.layout.partition()
|
||||
.size([2 * Math.PI, radius * radius])
|
||||
.value(function (d) { return d.m1; });
|
||||
|
||||
var arc = d3.svg.arc()
|
||||
.startAngle(function (d) {
|
||||
return d.x;
|
||||
})
|
||||
.endAngle(function (d) {
|
||||
return d.x + d.dx;
|
||||
})
|
||||
.innerRadius(function (d) {
|
||||
return Math.sqrt(d.y);
|
||||
})
|
||||
.outerRadius(function (d) {
|
||||
return Math.sqrt(d.y + d.dy);
|
||||
});
|
||||
|
||||
var formatNum = d3.format(".3s");
|
||||
var formatPerc = d3.format(".3p");
|
||||
|
||||
container.select("svg").remove();
|
||||
|
||||
var svg = container.append("svg:svg")
|
||||
.attr("width", containerWidth)
|
||||
.attr("height", containerHeight);
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, rawData) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
|
||||
createBreadcrumbs(rawData);
|
||||
createVisualization(rawData);
|
||||
|
||||
slice.done(rawData);
|
||||
});
|
||||
|
||||
function createBreadcrumbs(rawData) {
|
||||
var firstRowData = rawData.data[0];
|
||||
maxBreadcrumbs = (firstRowData.length - 2) + 1; // -2 bc row contains 2x metrics, +extra for %label and buffer
|
||||
|
||||
breadcrumbDims = {
|
||||
width: visWidth / maxBreadcrumbs,
|
||||
height: breadcrumbHeight *0.8, // more margin
|
||||
spacing: 3,
|
||||
tipTailWidth: 10
|
||||
};
|
||||
|
||||
breadcrumbs = svg.append("svg:g")
|
||||
.attr("class", "breadcrumbs")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
breadcrumbs.append("svg:text")
|
||||
.attr("class", "end-label");
|
||||
}
|
||||
|
||||
// Main function to draw and set up the visualization, once we have the data.
|
||||
function createVisualization(rawData) {
|
||||
var tree = buildHierarchy(rawData.data);
|
||||
|
||||
vis = svg.append("svg:g")
|
||||
.attr("class", "sunburst-vis")
|
||||
.attr("transform", "translate(" + (margin.left + (visWidth / 2)) + "," + (margin.top + breadcrumbHeight + (visHeight / 2)) + ")")
|
||||
.on("mouseleave", mouseleave);
|
||||
|
||||
arcs = vis.append("svg:g")
|
||||
.attr("id", "arcs");
|
||||
|
||||
gMiddleText = vis.append("svg:g")
|
||||
.attr("class", "center-label");
|
||||
|
||||
// Bounding circle underneath the sunburst, to make it easier to detect
|
||||
// when the mouse leaves the parent g.
|
||||
arcs.append("svg:circle")
|
||||
.attr("r", radius)
|
||||
.style("opacity", 0);
|
||||
|
||||
// For efficiency, filter nodes to keep only those large enough to see.
|
||||
var nodes = partition.nodes(tree)
|
||||
.filter(function (d) {
|
||||
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
|
||||
});
|
||||
|
||||
var ext;
|
||||
|
||||
if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
|
||||
colorByCategory = false;
|
||||
|
||||
ext = d3.extent(nodes, function (d) {
|
||||
return d.m2 / d.m1;
|
||||
});
|
||||
|
||||
colorScale = d3.scale.linear()
|
||||
.domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
|
||||
.range(["#00D1C1", "white", "#FFB400"]);
|
||||
}
|
||||
|
||||
var path = arcs.data([tree]).selectAll("path")
|
||||
.data(nodes)
|
||||
.enter().append("svg:path")
|
||||
.attr("display", function (d) {
|
||||
return d.depth ? null : "none";
|
||||
})
|
||||
.attr("d", arc)
|
||||
.attr("fill-rule", "evenodd")
|
||||
.style("fill", function (d) {
|
||||
return colorByCategory ? px.color.category21(d.name) : colorScale(d.m2 / d.m1);
|
||||
})
|
||||
.style("opacity", 1)
|
||||
.on("mouseenter", mouseenter);
|
||||
|
||||
// Get total size of the tree = value of root node from partition.
|
||||
totalSize = path.node().__data__.value;
|
||||
}
|
||||
|
||||
// Fade all but the current sequence, and show it in the breadcrumb trail.
|
||||
function mouseenter(d) {
|
||||
|
||||
var sequenceArray = getAncestors(d);
|
||||
var parentOfD = sequenceArray[sequenceArray.length - 2] || null;
|
||||
|
||||
var absolutePercentage = (d.m1 / totalSize).toPrecision(3);
|
||||
var conditionalPercentage = parentOfD ? (d.m1 / parentOfD.m1).toPrecision(3) : null;
|
||||
|
||||
var absolutePercString = formatPerc(absolutePercentage);
|
||||
var conditionalPercString = parentOfD ? formatPerc(conditionalPercentage) : "";
|
||||
|
||||
var yOffsets = ["-25", "7", "35", "60"]; // 3 levels of text if inner-most level, 4 otherwise
|
||||
var offsetIndex = 0;
|
||||
|
||||
// If metrics match, assume we are coloring by category
|
||||
var metricsMatch = Math.abs(d.m1 - d.m2) < 0.00001;
|
||||
|
||||
gMiddleText.selectAll("*").remove();
|
||||
|
||||
gMiddleText.append("text")
|
||||
.attr("class", "path-abs-percent")
|
||||
.attr("y", yOffsets[offsetIndex++])
|
||||
.text(absolutePercString + " of total");
|
||||
|
||||
if (conditionalPercString) {
|
||||
gMiddleText.append("text")
|
||||
.attr("class", "path-cond-percent")
|
||||
.attr("y", yOffsets[offsetIndex++])
|
||||
.text(conditionalPercString + " of parent");
|
||||
}
|
||||
|
||||
gMiddleText.append("text")
|
||||
.attr("class", "path-metrics")
|
||||
.attr("y", yOffsets[offsetIndex++])
|
||||
.text("m1: " + formatNum(d.m1) + (metricsMatch ? "" : ", m2: " + formatNum(d.m2)));
|
||||
|
||||
gMiddleText.append("text")
|
||||
.attr("class", "path-ratio")
|
||||
.attr("y", yOffsets[offsetIndex++])
|
||||
.text((metricsMatch ? "" : ("m2/m1: " + formatPerc(d.m2 / d.m1))) );
|
||||
|
||||
// Reset and fade all the segments.
|
||||
arcs.selectAll("path")
|
||||
.style("stroke-width", null)
|
||||
.style("stroke", null)
|
||||
.style("opacity", 0.7);
|
||||
|
||||
// Then highlight only those that are an ancestor of the current segment.
|
||||
arcs.selectAll("path")
|
||||
.filter(function (node) {
|
||||
return (sequenceArray.indexOf(node) >= 0);
|
||||
})
|
||||
.style("opacity", 1)
|
||||
.style("stroke-width", "2px")
|
||||
.style("stroke", "#000");
|
||||
|
||||
updateBreadcrumbs(sequenceArray, absolutePercString);
|
||||
}
|
||||
|
||||
// Restore everything to full opacity when moving off the visualization.
|
||||
function mouseleave(d) {
|
||||
|
||||
// Hide the breadcrumb trail
|
||||
breadcrumbs.style("visibility", "hidden");
|
||||
|
||||
gMiddleText.selectAll("*").remove();
|
||||
|
||||
// Deactivate all segments during transition.
|
||||
arcs.selectAll("path").on("mouseenter", null);
|
||||
//gMiddleText.selectAll("*").remove();
|
||||
|
||||
// Transition each segment to full opacity and then reactivate it.
|
||||
arcs.selectAll("path")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style("opacity", 1)
|
||||
.style("stroke", null)
|
||||
.style("stroke-width", null)
|
||||
.each("end", function () {
|
||||
d3.select(this).on("mouseenter", mouseenter);
|
||||
});
|
||||
}
|
||||
|
||||
// Given a node in a partition layout, return an array of all of its ancestor
|
||||
// nodes, highest first, but excluding the root.
|
||||
function getAncestors(node) {
|
||||
var path = [];
|
||||
var current = node;
|
||||
while (current.parent) {
|
||||
path.unshift(current);
|
||||
current = current.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// Generate a string that describes the points of a breadcrumb polygon.
|
||||
function breadcrumbPoints(d, i) {
|
||||
var points = [];
|
||||
points.push("0,0");
|
||||
points.push(breadcrumbDims.width + ",0");
|
||||
points.push(breadcrumbDims.width + breadcrumbDims.tipTailWidth + "," + (breadcrumbDims.height / 2));
|
||||
points.push(breadcrumbDims.width+ "," + breadcrumbDims.height);
|
||||
points.push("0," + breadcrumbDims.height);
|
||||
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
|
||||
points.push(breadcrumbDims.tipTailWidth + "," + (breadcrumbDims.height / 2));
|
||||
}
|
||||
return points.join(" ");
|
||||
}
|
||||
|
||||
function updateBreadcrumbs(sequenceArray, percentageString) {
|
||||
var g = breadcrumbs.selectAll("g")
|
||||
.data(sequenceArray, function (d) {
|
||||
return d.name + d.depth;
|
||||
});
|
||||
|
||||
// Add breadcrumb and label for entering nodes.
|
||||
var entering = g.enter().append("svg:g");
|
||||
|
||||
entering.append("svg:polygon")
|
||||
.attr("points", breadcrumbPoints)
|
||||
.style("fill", function (d) {
|
||||
return colorByCategory ? px.color.category21(d.name) : colorScale(d.m2 / d.m1);
|
||||
});
|
||||
|
||||
entering.append("svg:text")
|
||||
.attr("x", (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
|
||||
.attr("y", breadcrumbDims.height / 4)
|
||||
.attr("dy", "0.35em")
|
||||
.attr("class", "step-label")
|
||||
.text(function (d) { return d.name; })
|
||||
.call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
|
||||
|
||||
// Set position for entering and updating nodes.
|
||||
g.attr("transform", function (d, i) {
|
||||
return "translate(" + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ", 0)";
|
||||
});
|
||||
|
||||
// Remove exiting nodes.
|
||||
g.exit().remove();
|
||||
|
||||
// Now move and update the percentage at the end.
|
||||
breadcrumbs.select(".end-label")
|
||||
.attr("x", (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
|
||||
.attr("y", breadcrumbDims.height / 2)
|
||||
.attr("dy", "0.35em")
|
||||
.text(percentageString);
|
||||
|
||||
// Make the breadcrumb trail visible, if it's hidden.
|
||||
breadcrumbs.style("visibility", null);
|
||||
}
|
||||
|
||||
function buildHierarchy(rows) {
|
||||
var root = {
|
||||
name: "root",
|
||||
children: []
|
||||
};
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var row = rows[i];
|
||||
var m1 = Number(row[row.length - 2]);
|
||||
var m2 = Number(row[row.length - 1]);
|
||||
var levels = row.slice(0, row.length - 2);
|
||||
if (isNaN(m1)) { // e.g. if this is a header row
|
||||
continue;
|
||||
}
|
||||
var currentNode = root;
|
||||
for (var j = 0; j < levels.length; j++) {
|
||||
var children = currentNode.children || [];
|
||||
var nodeName = levels[j];
|
||||
// If the next node has the name "0", it will
|
||||
var isLeafNode = (j >= levels.length - 1) || levels[j+1] === 0;
|
||||
var childNode;
|
||||
|
||||
if (!isLeafNode) {
|
||||
// Not yet at the end of the sequence; move down the tree.
|
||||
var foundChild = false;
|
||||
for (var k = 0; k < children.length; k++) {
|
||||
if (children[k].name === nodeName) {
|
||||
childNode = children[k];
|
||||
foundChild = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// If we don't already have a child node for this branch, create it.
|
||||
if (!foundChild) {
|
||||
childNode = {
|
||||
name: nodeName,
|
||||
children: []
|
||||
};
|
||||
children.push(childNode);
|
||||
}
|
||||
currentNode = childNode;
|
||||
} else if (nodeName !== 0) {
|
||||
// Reached the end of the sequence; create a leaf node.
|
||||
childNode = {
|
||||
name: nodeName,
|
||||
m1: m1,
|
||||
m2: m2
|
||||
};
|
||||
children.push(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function recurse(node) {
|
||||
if (node.children) {
|
||||
var sums;
|
||||
var m1 = 0;
|
||||
var m2 = 0;
|
||||
for (var i = 0; i < node.children.length; i++) {
|
||||
sums = recurse(node.children[i]);
|
||||
m1 += sums[0];
|
||||
m2 += sums[1];
|
||||
}
|
||||
node.m1 = m1;
|
||||
node.m2 = m2;
|
||||
}
|
||||
return [node.m1, node.m2];
|
||||
}
|
||||
recurse(root);
|
||||
return root;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
render: render,
|
||||
resize: render
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = sunburstVis;
|
||||
@@ -1,126 +0,0 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var d3 = require('d3');
|
||||
|
||||
require('./table.css');
|
||||
require('datatables.net-bs');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
|
||||
function tableVis(slice) {
|
||||
var data = slice.data;
|
||||
var form_data = data.form_data;
|
||||
var f = d3.format('.3s');
|
||||
var fC = d3.format('0,000');
|
||||
|
||||
function refresh() {
|
||||
$.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError);
|
||||
|
||||
function onError(xhr) {
|
||||
slice.error(xhr.responseText);
|
||||
}
|
||||
|
||||
function onSuccess(json) {
|
||||
var data = json.data;
|
||||
var metrics = json.form_data.metrics;
|
||||
|
||||
function col(c) {
|
||||
var arr = [];
|
||||
for (var i = 0; i < data.records.length; i++) {
|
||||
arr.push(json.data.records[i][c]);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
var maxes = {};
|
||||
for (var i = 0; i < metrics.length; i++) {
|
||||
maxes[metrics[i]] = d3.max(col(metrics[i]));
|
||||
}
|
||||
|
||||
var table = d3.select(slice.selector).append('table')
|
||||
.classed('dataframe dataframe table table-striped table-bordered table-condensed table-hover dataTable no-footer', true)
|
||||
.attr('width', '100%');
|
||||
|
||||
table.append('thead').append('tr')
|
||||
.selectAll('th')
|
||||
.data(data.columns).enter()
|
||||
.append('th')
|
||||
.text(function (d) {
|
||||
return d;
|
||||
});
|
||||
|
||||
table.append('tbody')
|
||||
.selectAll('tr')
|
||||
.data(data.records).enter()
|
||||
.append('tr')
|
||||
.selectAll('td')
|
||||
.data(function (row, i) {
|
||||
return data.columns.map(function (c) {
|
||||
return {
|
||||
col: c,
|
||||
val: row[c],
|
||||
isMetric: metrics.indexOf(c) >= 0
|
||||
};
|
||||
});
|
||||
}).enter()
|
||||
.append('td')
|
||||
.style('background-image', function (d) {
|
||||
if (d.isMetric) {
|
||||
var perc = Math.round((d.val / maxes[d.col]) * 100);
|
||||
return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
|
||||
}
|
||||
})
|
||||
.attr('title', function (d) {
|
||||
if (!isNaN(d.val)) {
|
||||
return fC(d.val);
|
||||
}
|
||||
})
|
||||
.attr('data-sort', function (d) {
|
||||
if (d.isMetric) {
|
||||
return d.val;
|
||||
}
|
||||
})
|
||||
.on("click", function (d) {
|
||||
if (!d.isMetric) {
|
||||
var td = d3.select(this);
|
||||
if (td.classed('filtered')) {
|
||||
slice.removeFilter(d.col, [d.val]);
|
||||
d3.select(this).classed('filtered', false);
|
||||
} else {
|
||||
d3.select(this).classed('filtered', true);
|
||||
slice.addFilter(d.col, [d.val]);
|
||||
}
|
||||
}
|
||||
})
|
||||
.style("cursor", function (d) {
|
||||
if (!d.isMetric) {
|
||||
return 'pointer';
|
||||
}
|
||||
})
|
||||
.html(function (d) {
|
||||
if (d.isMetric) {
|
||||
return f(d.val);
|
||||
} else {
|
||||
return d.val;
|
||||
}
|
||||
});
|
||||
var datatable = slice.container.find('.dataTable').DataTable({
|
||||
paging: false,
|
||||
searching: form_data.include_search,
|
||||
bInfo: false
|
||||
});
|
||||
// Sorting table by main column
|
||||
if (form_data.metrics.length > 0) {
|
||||
var main_metric = form_data.metrics[0];
|
||||
datatable.column(data.columns.indexOf(main_metric)).order('desc').draw();
|
||||
}
|
||||
slice.done(json);
|
||||
slice.container.parents('.widget').find('.tooltip').remove();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: function () {}
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = tableVis;
|
||||
@@ -1,15 +0,0 @@
|
||||
.node {
|
||||
border: solid 1px white;
|
||||
font: 10px sans-serif;
|
||||
line-height: 12px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
text-indent: 2px;
|
||||
padding: 0px; /* form div giving top 1px */
|
||||
box-sizing: content-box; /* otherwise inheriting border-box */
|
||||
}
|
||||
|
||||
.treemap-container {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
// JS
|
||||
var d3 = window.d3 || require('d3');
|
||||
var px = window.px || require('../javascripts/modules/caravel.js');
|
||||
|
||||
// CSS
|
||||
require('./treemap.css');
|
||||
|
||||
/* Modified from https://bl.ocks.org/mbostock/4063582 */
|
||||
function treemap(slice) {
|
||||
|
||||
var div = d3.select(slice.selector);
|
||||
|
||||
var _draw = function (data, eltWidth, eltHeight, includeTitle) {
|
||||
|
||||
var margin = { top: 0, right: 0, bottom: 0, left: 0 },
|
||||
headerHeight = includeTitle ? 30 : 0,
|
||||
width = eltWidth - margin.left - margin.right,
|
||||
height = eltHeight - headerHeight - margin.top - margin.bottom;
|
||||
|
||||
var treemap = d3.layout.treemap()
|
||||
.size([width, height])
|
||||
.value(function (d) { return d.value; });
|
||||
|
||||
var root = div.append("div")
|
||||
.classed("treemap-container", true);
|
||||
|
||||
var header = root.append("div")
|
||||
.style("width", (width + margin.left + margin.right) + "px")
|
||||
.style("height", headerHeight + "px");
|
||||
|
||||
var container = root.append("div")
|
||||
.style("position", "relative")
|
||||
.style("width", (width + margin.left + margin.right) + "px")
|
||||
.style("height", (height + margin.top + margin.bottom) + "px")
|
||||
.style("left", margin.left + "px")
|
||||
.style("top", margin.top + "px");
|
||||
|
||||
var position = function (selection) {
|
||||
selection.style("left", function (d) { return d.x + "px"; })
|
||||
.style("top", function (d) { return d.y + "px"; })
|
||||
.style("width", function (d) { return Math.max(0, d.dx - 1) + "px"; })
|
||||
.style("height", function (d) { return Math.max(0, d.dy - 1) + "px"; });
|
||||
};
|
||||
|
||||
container.datum(data).selectAll(".node")
|
||||
.data(treemap.nodes)
|
||||
.enter().append("div")
|
||||
.attr("class", "node")
|
||||
.call(position)
|
||||
.style("background", function (d) {
|
||||
return d.children ? px.color.category21(d.name) : null;
|
||||
})
|
||||
.style("color", function (d) {
|
||||
// detect if our background is dark and we need a
|
||||
// light text color or vice-versa
|
||||
var bg = d.parent ? px.color.category21(d.parent.name) : null;
|
||||
if (bg) {
|
||||
return d3.hsl(bg).l < 0.35 ? '#d3d3d3' : '#111111';
|
||||
}
|
||||
})
|
||||
.text(function (d) { return d.children ? null : d.name; });
|
||||
|
||||
if (includeTitle) {
|
||||
// title to help with multiple metrics (if any)
|
||||
header.append("span")
|
||||
.style("font-size", "18px")
|
||||
.style("font-weight", "bold")
|
||||
.text(data.name);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var render = function () {
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
|
||||
div.selectAll("*").remove();
|
||||
var width = slice.width();
|
||||
// facet muliple metrics (no sense in combining)
|
||||
var height = slice.height() / json.data.length;
|
||||
var includeTitles = json.data.length > 1;
|
||||
for (var i = 0, l = json.data.length; i < l; i ++) {
|
||||
_draw(json.data[i], width, height, includeTitles);
|
||||
}
|
||||
|
||||
slice.done(json);
|
||||
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
render: render,
|
||||
resize: render
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = treemap;
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
var px = window.px || require('../javascripts/modules/caravel.js');
|
||||
var d3 = window.d3 || require('d3');
|
||||
var cloudLayout = require('d3-cloud');
|
||||
|
||||
function wordCloudChart(slice) {
|
||||
var chart = d3.select(slice.selector);
|
||||
|
||||
function refresh() {
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var data = json.data;
|
||||
var range = [
|
||||
json.form_data.size_from,
|
||||
json.form_data.size_to
|
||||
];
|
||||
var rotation = json.form_data.rotation;
|
||||
var f_rotation;
|
||||
if (rotation === "square") {
|
||||
f_rotation = function () {
|
||||
return ~~(Math.random() * 2) * 90;
|
||||
};
|
||||
} else if (rotation === "flat") {
|
||||
f_rotation = function () {
|
||||
return 0;
|
||||
};
|
||||
} else {
|
||||
f_rotation = function () {
|
||||
return (~~(Math.random() * 6) - 3) * 30;
|
||||
};
|
||||
}
|
||||
var size = [slice.width(), slice.height()];
|
||||
|
||||
var scale = d3.scale.linear()
|
||||
.range(range)
|
||||
.domain(d3.extent(data, function (d) {
|
||||
return d.size;
|
||||
}));
|
||||
|
||||
var layout = cloudLayout()
|
||||
.size(size)
|
||||
.words(data)
|
||||
.padding(5)
|
||||
.rotate(f_rotation)
|
||||
.font("serif")
|
||||
.fontSize(function (d) {
|
||||
return scale(d.size);
|
||||
})
|
||||
.on("end", draw);
|
||||
|
||||
layout.start();
|
||||
|
||||
function draw(words) {
|
||||
chart.selectAll("*").remove();
|
||||
|
||||
chart.append("svg")
|
||||
.attr("width", layout.size()[0])
|
||||
.attr("height", layout.size()[1])
|
||||
.append("g")
|
||||
.attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")")
|
||||
.selectAll("text")
|
||||
.data(words)
|
||||
.enter().append("text")
|
||||
.style("font-size", function (d) {
|
||||
return d.size + "px";
|
||||
})
|
||||
.style("font-family", "Impact")
|
||||
.style("fill", function (d) {
|
||||
return px.color.category21(d.text);
|
||||
})
|
||||
.attr("text-anchor", "middle")
|
||||
.attr("transform", function (d) {
|
||||
return "translate(" + [d.x, d.y] + ") rotate(" + d.rotate + ")";
|
||||
})
|
||||
.text(function (d) {
|
||||
return d.text;
|
||||
});
|
||||
}
|
||||
slice.done(json);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
render: refresh,
|
||||
resize: refresh
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = wordCloudChart;
|
||||
@@ -1,110 +0,0 @@
|
||||
// JS
|
||||
var d3 = window.d3 || require('d3');
|
||||
//var Datamap = require('../vendor/datamaps/datamaps.all.js');
|
||||
var Datamap = require('datamaps');
|
||||
|
||||
// CSS
|
||||
require('./world_map.css');
|
||||
|
||||
function worldMapChart(slice) {
|
||||
var render = function () {
|
||||
var container = slice.container;
|
||||
var div = d3.select(slice.selector);
|
||||
|
||||
container.css('height', slice.height());
|
||||
|
||||
d3.json(slice.jsonEndpoint(), function (error, json) {
|
||||
var fd = json.form_data;
|
||||
|
||||
if (error !== null) {
|
||||
slice.error(error.responseText);
|
||||
return '';
|
||||
}
|
||||
var ext = d3.extent(json.data, function (d) {
|
||||
return d.m1;
|
||||
});
|
||||
var extRadius = d3.extent(json.data, function (d) {
|
||||
return d.m2;
|
||||
});
|
||||
var radiusScale = d3.scale.linear()
|
||||
.domain([extRadius[0], extRadius[1]])
|
||||
.range([1, fd.max_bubble_size]);
|
||||
|
||||
json.data.forEach(function (d) {
|
||||
d.radius = radiusScale(d.m2);
|
||||
});
|
||||
|
||||
var colorScale = d3.scale.linear()
|
||||
.domain([ext[0], ext[1]])
|
||||
.range(["#FFF", "black"]);
|
||||
|
||||
var d = {};
|
||||
for (var i = 0; i < json.data.length; i++) {
|
||||
var country = json.data[i];
|
||||
country.fillColor = colorScale(country.m1);
|
||||
d[country.country] = country;
|
||||
}
|
||||
|
||||
var f = d3.format('.3s');
|
||||
|
||||
container.show();
|
||||
|
||||
var map = new Datamap({
|
||||
element: slice.container.get(0),
|
||||
data: json.data,
|
||||
fills: {
|
||||
defaultFill: '#ddd'
|
||||
},
|
||||
geographyConfig: {
|
||||
popupOnHover: true,
|
||||
highlightOnHover: true,
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff',
|
||||
highlightBorderColor: '#fff',
|
||||
highlightFillColor: '#005a63',
|
||||
highlightBorderWidth: 1,
|
||||
popupTemplate: function (geo, data) {
|
||||
return '<div class="hoverinfo"><strong>' + data.name + '</strong><br>' + f(data.m1) + '</div>';
|
||||
}
|
||||
},
|
||||
bubblesConfig: {
|
||||
borderWidth: 1,
|
||||
borderOpacity: 1,
|
||||
borderColor: '#005a63',
|
||||
popupOnHover: true,
|
||||
radius: null,
|
||||
popupTemplate: function (geo, data) {
|
||||
return '<div class="hoverinfo"><strong>' + data.name + '</strong><br>' + f(data.m2) + '</div>';
|
||||
},
|
||||
fillOpacity: 0.5,
|
||||
animate: true,
|
||||
highlightOnHover: true,
|
||||
highlightFillColor: '#005a63',
|
||||
highlightBorderColor: 'black',
|
||||
highlightBorderWidth: 2,
|
||||
highlightBorderOpacity: 1,
|
||||
highlightFillOpacity: 0.85,
|
||||
exitDelay: 100,
|
||||
key: JSON.stringify
|
||||
}
|
||||
});
|
||||
|
||||
map.updateChoropleth(d);
|
||||
|
||||
if (fd.show_bubbles) {
|
||||
map.bubbles(json.data);
|
||||
div.selectAll("circle.datamaps-bubble").style('fill', '#005a63');
|
||||
}
|
||||
|
||||
slice.done(json);
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
render: render,
|
||||
resize: render
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = worldMapChart;
|
||||
@@ -1,51 +0,0 @@
|
||||
var path = require('path');
|
||||
var APP_DIR = path.resolve(__dirname, './'); // input
|
||||
var BUILD_DIR = path.resolve(__dirname, './javascripts/dist'); // output
|
||||
|
||||
var config = {
|
||||
// for now generate one compiled js file per entry point / html page
|
||||
entry: {
|
||||
'css-theme': APP_DIR + '/javascripts/css-theme.js',
|
||||
dashboard: APP_DIR + '/javascripts/dashboard.js',
|
||||
explore: APP_DIR + '/javascripts/explore.js',
|
||||
welcome: APP_DIR + '/javascripts/welcome.js',
|
||||
sql: APP_DIR + '/javascripts/sql.js',
|
||||
standalone: APP_DIR + '/javascripts/standalone.js'
|
||||
},
|
||||
output: {
|
||||
path: BUILD_DIR,
|
||||
filename: '[name].entry.js'
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
{
|
||||
test: /\.jsx?/,
|
||||
include: APP_DIR,
|
||||
exclude: APP_DIR + '/node_modules',
|
||||
loader: 'babel'
|
||||
},
|
||||
/* for require('*.css') */
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: APP_DIR,
|
||||
loader: "style-loader!css-loader"
|
||||
},
|
||||
/* for css linking images */
|
||||
{ test: /\.png$/, loader: "url-loader?limit=100000" },
|
||||
{ test: /\.jpg$/, loader: "file-loader" },
|
||||
{ test: /\.gif$/, loader: "file-loader" },
|
||||
/* for font-awesome */
|
||||
{ test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff" },
|
||||
{ test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" },
|
||||
/* for require('*.less') */
|
||||
{
|
||||
test: /\.less$/,
|
||||
include: APP_DIR,
|
||||
loader: "style!css!less"
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: []
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,109 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from subprocess import Popen
|
||||
import textwrap
|
||||
|
||||
from flask.ext.migrate import MigrateCommand
|
||||
from flask.ext.script import Manager
|
||||
|
||||
import caravel
|
||||
from caravel import app, ascii_art, db, data, utils
|
||||
|
||||
config = app.config
|
||||
|
||||
manager = Manager(app)
|
||||
manager.add_command('db', MigrateCommand)
|
||||
|
||||
|
||||
@manager.option(
|
||||
'-d', '--debug', action='store_true',
|
||||
help="Start the web server in debug mode")
|
||||
@manager.option(
|
||||
'-p', '--port', default=config.get("CARAVEL_WEBSERVER_PORT"),
|
||||
help="Specify the port on which to run the web server")
|
||||
@manager.option(
|
||||
'-w', '--workers', default=config.get("CARAVEL_WORKERS", 16),
|
||||
help="Number of gunicorn web server workers to fire up")
|
||||
@manager.option(
|
||||
'-t', '--timeout', default=config.get("CARAVEL_WEBSERVER_TIMEOUT"),
|
||||
help="Specify the timeout (seconds) for the gunicorn web server")
|
||||
def runserver(debug, port, timeout, workers):
|
||||
"""Starts a Caravel web server"""
|
||||
debug = debug or config.get("DEBUG")
|
||||
if debug:
|
||||
app.run(
|
||||
host='0.0.0.0',
|
||||
port=int(port),
|
||||
debug=True)
|
||||
else:
|
||||
cmd = (
|
||||
"gunicorn "
|
||||
"-w {workers} "
|
||||
"--timeout {timeout} "
|
||||
"-b 0.0.0.0:{port} "
|
||||
"caravel:app").format(**locals())
|
||||
print("Starting server with command: " + cmd)
|
||||
Popen(cmd, shell=True).wait()
|
||||
|
||||
@manager.command
|
||||
def init():
|
||||
"""Inits the Caravel application"""
|
||||
utils.init(caravel)
|
||||
|
||||
@manager.command
|
||||
def version():
|
||||
"""Prints the current version number"""
|
||||
s = (
|
||||
"\n{boat}\n\n"
|
||||
"-----------------------\n"
|
||||
"Caravel {version}\n"
|
||||
"-----------------------\n").format(
|
||||
boat=ascii_art.boat, version=caravel.VERSION)
|
||||
print(s)
|
||||
|
||||
@manager.option(
|
||||
'-s', '--sample', action='store_true',
|
||||
help="Only load 1000 rows (faster, used for testing)")
|
||||
def load_examples(sample):
|
||||
"""Loads a set of Slices and Dashboards and a supporting dataset """
|
||||
print("Loading examples into {}".format(db))
|
||||
|
||||
data.load_css_templates()
|
||||
|
||||
print("Loading energy related dataset")
|
||||
data.load_energy()
|
||||
|
||||
print("Loading [World Bank's Health Nutrition and Population Stats]")
|
||||
data.load_world_bank_health_n_pop()
|
||||
|
||||
print("Loading [Birth names]")
|
||||
data.load_birth_names()
|
||||
|
||||
@manager.command
|
||||
def refresh_druid():
|
||||
"""Refresh all druid datasources"""
|
||||
session = db.session()
|
||||
from caravel import models
|
||||
for cluster in session.query(models.DruidCluster).all():
|
||||
try:
|
||||
cluster.refresh_datasources()
|
||||
except Exception as e:
|
||||
print(
|
||||
"Error while processing cluster '{}'\n{}".format(
|
||||
cluster, str(e)))
|
||||
logging.exception(e)
|
||||
cluster.metadata_last_refreshed = datetime.now()
|
||||
print(
|
||||
"Refreshed metadata from cluster "
|
||||
"[" + cluster.cluster_name + "]")
|
||||
session.commit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
manager.run()
|
||||
@@ -1,127 +0,0 @@
|
||||
"""The main config file for Caravel
|
||||
|
||||
All configuration in this file can be overridden by providing a local_config
|
||||
in your PYTHONPATH as there is a ``from local_config import *``
|
||||
at the end of this file.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import os
|
||||
|
||||
from dateutil import tz
|
||||
from flask_appbuilder.security.manager import AUTH_DB
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# Caravel specifix config
|
||||
# ---------------------------------------------------------
|
||||
ROW_LIMIT = 50000
|
||||
WEBSERVER_THREADS = 8
|
||||
|
||||
CARAVEL_WEBSERVER_PORT = 8088
|
||||
CARAVEL_WEBSERVER_TIMEOUT = 60
|
||||
|
||||
CUSTOM_SECURITY_MANAGER = None
|
||||
# ---------------------------------------------------------
|
||||
|
||||
# Your App secret key
|
||||
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' # noqa
|
||||
|
||||
# The SQLAlchemy connection string.
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/caravel.db'
|
||||
# SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp'
|
||||
# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
|
||||
|
||||
# Flask-WTF flag for CSRF
|
||||
CSRF_ENABLED = True
|
||||
|
||||
# Whether to run the web server in debug mode or not
|
||||
DEBUG = False
|
||||
|
||||
# Whether to show the stacktrace on 500 error
|
||||
SHOW_STACKTRACE = True
|
||||
|
||||
# ------------------------------
|
||||
# GLOBALS FOR APP Builder
|
||||
# ------------------------------
|
||||
# Uncomment to setup Your App name
|
||||
APP_NAME = "Caravel"
|
||||
|
||||
# Uncomment to setup Setup an App icon
|
||||
# APP_ICON = "/static/img/something.png"
|
||||
|
||||
# Druid query timezone
|
||||
# tz.tzutc() : Using utc timezone
|
||||
# tz.tzlocal() : Using local timezone
|
||||
# other tz can be overridden by providing a local_config
|
||||
DRUID_IS_ACTIVE = True
|
||||
DRUID_TZ = tz.tzutc()
|
||||
|
||||
# ----------------------------------------------------
|
||||
# AUTHENTICATION CONFIG
|
||||
# ----------------------------------------------------
|
||||
# The authentication type
|
||||
# AUTH_OID : Is for OpenID
|
||||
# AUTH_DB : Is for database (username/password()
|
||||
# AUTH_LDAP : Is for LDAP
|
||||
# AUTH_REMOTE_USER : Is for using REMOTE_USER from web server
|
||||
AUTH_TYPE = AUTH_DB
|
||||
|
||||
# Uncomment to setup Full admin role name
|
||||
# AUTH_ROLE_ADMIN = 'Admin'
|
||||
|
||||
# Uncomment to setup Public role name, no authentication needed
|
||||
# AUTH_ROLE_PUBLIC = 'Public'
|
||||
|
||||
# Will allow user self registration
|
||||
# AUTH_USER_REGISTRATION = True
|
||||
|
||||
# The default user self registration role
|
||||
# AUTH_USER_REGISTRATION_ROLE = "Public"
|
||||
|
||||
# When using LDAP Auth, setup the ldap server
|
||||
# AUTH_LDAP_SERVER = "ldap://ldapserver.new"
|
||||
|
||||
# Uncomment to setup OpenID providers example for OpenID authentication
|
||||
# OPENID_PROVIDERS = [
|
||||
# { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' },
|
||||
# { 'name': 'AOL', 'url': 'http://openid.aol.com/<username>' },
|
||||
# { 'name': 'Flickr', 'url': 'http://www.flickr.com/<username>' },
|
||||
# { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }]
|
||||
# ---------------------------------------------------
|
||||
# Babel config for translations
|
||||
# ---------------------------------------------------
|
||||
# Setup default language
|
||||
BABEL_DEFAULT_LOCALE = 'en'
|
||||
# Your application default translation path
|
||||
BABEL_DEFAULT_FOLDER = 'translations'
|
||||
# The allowed translation for you app
|
||||
LANGUAGES = {
|
||||
'en': {'flag': 'us', 'name': 'English'},
|
||||
}
|
||||
# ---------------------------------------------------
|
||||
# Image and file configuration
|
||||
# ---------------------------------------------------
|
||||
# The file upload folder, when using models with files
|
||||
UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/'
|
||||
|
||||
# The image upload folder, when using models with images
|
||||
IMG_UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/'
|
||||
|
||||
# The image upload url, when using models with images
|
||||
IMG_UPLOAD_URL = '/static/uploads/'
|
||||
# Setup image size default is (300, 200, True)
|
||||
# IMG_SIZE = (300, 200, True)
|
||||
|
||||
CACHE_DEFAULT_TIMEOUT = None
|
||||
CACHE_CONFIG = {'CACHE_TYPE': 'null'}
|
||||
|
||||
try:
|
||||
from caravel_config import * # noqa
|
||||
except Exception:
|
||||
pass
|
||||
@@ -1,730 +0,0 @@
|
||||
"""Loads datasets, dashboards and slices in a new caravel instance"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
import pandas as pd
|
||||
from sqlalchemy import String, DateTime, Float
|
||||
|
||||
from caravel import app, db, models, utils
|
||||
|
||||
# Shortcuts
|
||||
DB = models.Database
|
||||
Slice = models.Slice
|
||||
TBL = models.SqlaTable
|
||||
Dash = models.Dashboard
|
||||
|
||||
config = app.config
|
||||
|
||||
DATA_FOLDER = os.path.join(config.get("BASE_DIR"), 'data')
|
||||
|
||||
|
||||
def get_or_create_db(session):
|
||||
print("Creating database reference")
|
||||
dbobj = session.query(DB).filter_by(database_name='main').first()
|
||||
if not dbobj:
|
||||
dbobj = DB(database_name="main")
|
||||
print(config.get("SQLALCHEMY_DATABASE_URI"))
|
||||
dbobj.sqlalchemy_uri = config.get("SQLALCHEMY_DATABASE_URI")
|
||||
session.add(dbobj)
|
||||
session.commit()
|
||||
return dbobj
|
||||
|
||||
|
||||
def merge_slice(slc):
|
||||
o = db.session.query(Slice).filter_by(slice_name=slc.slice_name).first()
|
||||
if o:
|
||||
db.session.delete(o)
|
||||
db.session.add(slc)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def get_slice_json(defaults, **kwargs):
|
||||
d = defaults.copy()
|
||||
d.update(kwargs)
|
||||
return json.dumps(d, indent=4, sort_keys=True)
|
||||
|
||||
|
||||
def load_energy():
|
||||
"""Loads an energy related dataset to use with sankey and graphs"""
|
||||
tbl_name = 'energy_usage'
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'energy.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.to_sql(
|
||||
tbl_name,
|
||||
db.engine,
|
||||
if_exists='replace',
|
||||
chunksize=500,
|
||||
dtype={
|
||||
'source': String(255),
|
||||
'target': String(255),
|
||||
'value': Float(),
|
||||
},
|
||||
index=False)
|
||||
|
||||
print("Creating table [wb_health_population] reference")
|
||||
tbl = db.session.query(TBL).filter_by(table_name=tbl_name).first()
|
||||
if not tbl:
|
||||
tbl = TBL(table_name=tbl_name)
|
||||
tbl.description = "Energy consumption"
|
||||
tbl.is_featured = True
|
||||
tbl.database = get_or_create_db(db.session)
|
||||
db.session.merge(tbl)
|
||||
db.session.commit()
|
||||
tbl.fetch_metadata()
|
||||
|
||||
merge_slice(
|
||||
Slice(
|
||||
slice_name="Energy Sankey",
|
||||
viz_type='sankey',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=textwrap.dedent("""\
|
||||
{
|
||||
"collapsed_fieldsets": "",
|
||||
"datasource_id": "3",
|
||||
"datasource_name": "energy_usage",
|
||||
"datasource_type": "table",
|
||||
"flt_col_0": "source",
|
||||
"flt_eq_0": "",
|
||||
"flt_op_0": "in",
|
||||
"groupby": [
|
||||
"source",
|
||||
"target"
|
||||
],
|
||||
"having": "",
|
||||
"metric": "sum__value",
|
||||
"row_limit": "5000",
|
||||
"slice_id": "",
|
||||
"slice_name": "Energy Sankey",
|
||||
"viz_type": "sankey",
|
||||
"where": ""
|
||||
}
|
||||
"""))
|
||||
)
|
||||
|
||||
|
||||
def load_world_bank_health_n_pop():
|
||||
"""Loads the world bank health dataset, slices and a dashboard"""
|
||||
tbl_name = 'wb_health_population'
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'countries.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.columns = [col.replace('.', '_') for col in pdf.columns]
|
||||
pdf.year = pd.to_datetime(pdf.year)
|
||||
pdf.to_sql(
|
||||
tbl_name,
|
||||
db.engine,
|
||||
if_exists='replace',
|
||||
chunksize=500,
|
||||
dtype={
|
||||
'year': DateTime(),
|
||||
'country_code': String(3),
|
||||
'country_name': String(255),
|
||||
'region': String(255),
|
||||
},
|
||||
index=False)
|
||||
|
||||
print("Creating table [wb_health_population] reference")
|
||||
tbl = db.session.query(TBL).filter_by(table_name=tbl_name).first()
|
||||
if not tbl:
|
||||
tbl = TBL(table_name=tbl_name)
|
||||
tbl.description = utils.readfile(os.path.join(DATA_FOLDER, 'countries.md'))
|
||||
tbl.main_dttm_col = 'year'
|
||||
tbl.is_featured = True
|
||||
tbl.database = get_or_create_db(db.session)
|
||||
db.session.merge(tbl)
|
||||
db.session.commit()
|
||||
tbl.fetch_metadata()
|
||||
|
||||
defaults = {
|
||||
"compare_lag": "10",
|
||||
"compare_suffix": "o10Y",
|
||||
"datasource_id": "1",
|
||||
"datasource_name": "birth_names",
|
||||
"datasource_type": "table",
|
||||
"limit": "25",
|
||||
"granularity": "year",
|
||||
"groupby": [],
|
||||
"metric": 'sum__SP_POP_TOTL',
|
||||
"metrics": ["sum__SP_POP_TOTL"],
|
||||
"row_limit": config.get("ROW_LIMIT"),
|
||||
"since": "2014-01-01",
|
||||
"until": "2014-01-01",
|
||||
"where": "",
|
||||
"markup_type": "markdown",
|
||||
"country_fieldtype": "cca3",
|
||||
"secondary_metric": "sum__SP_POP_TOTL",
|
||||
"entity": "country_code",
|
||||
"show_bubbles": "y",
|
||||
}
|
||||
|
||||
print("Creating slices")
|
||||
slices = [
|
||||
Slice(
|
||||
slice_name="Region Filter",
|
||||
viz_type='filter_box',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type='filter_box',
|
||||
groupby=['region', 'country_name'])),
|
||||
Slice(
|
||||
slice_name="World's Population",
|
||||
viz_type='big_number',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
since='2000',
|
||||
viz_type='big_number',
|
||||
compare_lag="10",
|
||||
metric='sum__SP_POP_TOTL',
|
||||
compare_suffix="over 10Y")),
|
||||
Slice(
|
||||
slice_name="Most Populated Countries",
|
||||
viz_type='table',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type='table',
|
||||
metrics=["sum__SP_POP_TOTL"],
|
||||
groupby=['country_name'])),
|
||||
Slice(
|
||||
slice_name="Growth Rate",
|
||||
viz_type='line',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type='line',
|
||||
since="1960-01-01",
|
||||
metrics=["sum__SP_POP_TOTL"],
|
||||
num_period_compare="10",
|
||||
groupby=['country_name'])),
|
||||
Slice(
|
||||
slice_name="% Rural",
|
||||
viz_type='world_map',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type='world_map',
|
||||
metric="sum__SP_RUR_TOTL_ZS",
|
||||
num_period_compare="10")),
|
||||
Slice(
|
||||
slice_name="Life Expexctancy VS Rural %",
|
||||
viz_type='bubble',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type='bubble',
|
||||
since="2011-01-01",
|
||||
until="2011-01-01",
|
||||
series="region",
|
||||
limit="0",
|
||||
entity="country_name",
|
||||
x="sum__SP_RUR_TOTL_ZS",
|
||||
y="sum__SP_DYN_LE00_IN",
|
||||
size="sum__SP_POP_TOTL",
|
||||
max_bubble_size="50",
|
||||
flt_col_1="country_code",
|
||||
flt_op_1="not in",
|
||||
flt_eq_1="TCA,MNP,DMA,MHL,MCO,SXM,CYM,TUV,IMY,KNA,ASM,ADO,AMA,PLW",
|
||||
num_period_compare="10",)),
|
||||
Slice(
|
||||
slice_name="Rural Breakdown",
|
||||
viz_type='sunburst',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type='sunburst',
|
||||
groupby=["region", "country_name"],
|
||||
secondary_metric="sum__SP_RUR_TOTL",
|
||||
since="2011-01-01",
|
||||
until="2011-01-01",)),
|
||||
Slice(
|
||||
slice_name="World's Pop Growth",
|
||||
viz_type='area',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
since="1960-01-01",
|
||||
until="now",
|
||||
viz_type='area',
|
||||
groupby=["region"],)),
|
||||
Slice(
|
||||
slice_name="Box plot",
|
||||
viz_type='box_plot',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
since="1960-01-01",
|
||||
until="now",
|
||||
whisker_options="Tukey",
|
||||
viz_type='box_plot',
|
||||
groupby=["region"],)),
|
||||
Slice(
|
||||
slice_name="Treemap",
|
||||
viz_type='treemap',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
since="1960-01-01",
|
||||
until="now",
|
||||
viz_type='treemap',
|
||||
metrics=["sum__SP_POP_TOTL"],
|
||||
groupby=["region", "country_code"],)),
|
||||
]
|
||||
for slc in slices:
|
||||
merge_slice(slc)
|
||||
|
||||
print("Creating a World's Health Bank dashboard")
|
||||
dash_name = "World's Health Bank Dashboard"
|
||||
dash = db.session.query(Dash).filter_by(dashboard_title=dash_name).first()
|
||||
|
||||
if not dash:
|
||||
dash = Dash()
|
||||
js = textwrap.dedent("""\
|
||||
[
|
||||
{
|
||||
"size_y": 2,
|
||||
"size_x": 3,
|
||||
"col": 10,
|
||||
"slice_id": "22",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 3,
|
||||
"col": 10,
|
||||
"slice_id": "23",
|
||||
"row": 3
|
||||
},
|
||||
{
|
||||
"size_y": 8,
|
||||
"size_x": 3,
|
||||
"col": 1,
|
||||
"slice_id": "24",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 6,
|
||||
"col": 4,
|
||||
"slice_id": "25",
|
||||
"row": 6
|
||||
},
|
||||
{
|
||||
"size_y": 5,
|
||||
"size_x": 6,
|
||||
"col": 4,
|
||||
"slice_id": "26",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 6,
|
||||
"col": 7,
|
||||
"slice_id": "27",
|
||||
"row": 9
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 3,
|
||||
"col": 10,
|
||||
"slice_id": "28",
|
||||
"row": 6
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 6,
|
||||
"col": 1,
|
||||
"slice_id": "29",
|
||||
"row": 9
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 5,
|
||||
"col": 8,
|
||||
"slice_id": "30",
|
||||
"row": 13
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 7,
|
||||
"col": 1,
|
||||
"slice_id": "31",
|
||||
"row": 13
|
||||
}
|
||||
]
|
||||
""")
|
||||
l = json.loads(js)
|
||||
for i, pos in enumerate(l):
|
||||
pos['slice_id'] = str(slices[i].id)
|
||||
|
||||
dash.dashboard_title = dash_name
|
||||
dash.position_json = json.dumps(l, indent=4)
|
||||
dash.slug = "world_health"
|
||||
|
||||
dash.slices = slices
|
||||
db.session.merge(dash)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def load_css_templates():
|
||||
"""Loads 2 css templates to demonstrate the feature"""
|
||||
print('Creating default CSS templates')
|
||||
CSS = models.CssTemplate # noqa
|
||||
|
||||
obj = db.session.query(CSS).filter_by(template_name='Flat').first()
|
||||
if not obj:
|
||||
obj = CSS(template_name="Flat")
|
||||
css = textwrap.dedent("""\
|
||||
.gridster li.widget {
|
||||
transition: background-color 0.5s ease;
|
||||
background-color: #FAFAFA;
|
||||
border: 1px solid #CCC;
|
||||
box-shadow: none;
|
||||
border-radius: 0px;
|
||||
}
|
||||
.gridster li.widget:hover {
|
||||
border: 1px solid #000;
|
||||
background-color: #EAEAEA;
|
||||
}
|
||||
.navbar {
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0.05;
|
||||
}
|
||||
.navbar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.chart-header .header{
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
}
|
||||
/*
|
||||
var bnbColors = [
|
||||
//rausch hackb kazan babu lima beach tirol
|
||||
'#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
|
||||
'#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a',
|
||||
'#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e',
|
||||
];
|
||||
*/
|
||||
""")
|
||||
obj.css = css
|
||||
db.session.merge(obj)
|
||||
db.session.commit()
|
||||
|
||||
obj = (
|
||||
db.session.query(CSS).filter_by(template_name='Courier Black').first())
|
||||
if not obj:
|
||||
obj = CSS(template_name="Courier Black")
|
||||
css = textwrap.dedent("""\
|
||||
.gridster li.widget {
|
||||
transition: background-color 0.5s ease;
|
||||
background-color: #EEE;
|
||||
border: 2px solid #444;
|
||||
border-radius: 15px;
|
||||
box-shadow: none;
|
||||
}
|
||||
h2 {
|
||||
color: white;
|
||||
font-size: 52px;
|
||||
}
|
||||
.navbar {
|
||||
box-shadow: none;
|
||||
}
|
||||
.gridster li.widget:hover {
|
||||
border: 2px solid #000;
|
||||
background-color: #EAEAEA;
|
||||
}
|
||||
.navbar {
|
||||
transition: opacity 0.5s ease;
|
||||
opacity: 0.05;
|
||||
}
|
||||
.navbar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.chart-header .header{
|
||||
font-weight: normal;
|
||||
font-size: 12px;
|
||||
}
|
||||
.nvd3 text {
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
body{
|
||||
background: #000;
|
||||
font-family: Courier, Monaco, monospace;;
|
||||
}
|
||||
/*
|
||||
var bnbColors = [
|
||||
//rausch hackb kazan babu lima beach tirol
|
||||
'#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
|
||||
'#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a',
|
||||
'#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e',
|
||||
];
|
||||
*/
|
||||
""")
|
||||
obj.css = css
|
||||
db.session.merge(obj)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def load_birth_names():
|
||||
"""Loading birth name dataset from a zip file in the repo"""
|
||||
with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f:
|
||||
pdf = pd.read_json(f)
|
||||
pdf.ds = pd.to_datetime(pdf.ds, unit='ms')
|
||||
pdf.to_sql(
|
||||
'birth_names',
|
||||
db.engine,
|
||||
if_exists='replace',
|
||||
chunksize=500,
|
||||
dtype={
|
||||
'ds': DateTime,
|
||||
'gender': String(16),
|
||||
'state': String(10),
|
||||
'name': String(255),
|
||||
},
|
||||
index=False)
|
||||
l = []
|
||||
print("Done loading table!")
|
||||
print("-" * 80)
|
||||
|
||||
print("Creating table reference")
|
||||
obj = db.session.query(TBL).filter_by(table_name='birth_names').first()
|
||||
if not obj:
|
||||
obj = TBL(table_name='birth_names')
|
||||
obj.main_dttm_col = 'ds'
|
||||
obj.database = get_or_create_db(db.session)
|
||||
obj.is_featured = True
|
||||
db.session.merge(obj)
|
||||
db.session.commit()
|
||||
obj.fetch_metadata()
|
||||
tbl = obj
|
||||
|
||||
defaults = {
|
||||
"compare_lag": "10",
|
||||
"compare_suffix": "o10Y",
|
||||
"datasource_id": "1",
|
||||
"datasource_name": "birth_names",
|
||||
"datasource_type": "table",
|
||||
"flt_op_1": "in",
|
||||
"limit": "25",
|
||||
"granularity": "ds",
|
||||
"groupby": [],
|
||||
"metric": 'sum__num',
|
||||
"metrics": ["sum__num"],
|
||||
"row_limit": config.get("ROW_LIMIT"),
|
||||
"since": "100 years ago",
|
||||
"until": "now",
|
||||
"viz_type": "table",
|
||||
"where": "",
|
||||
"markup_type": "markdown",
|
||||
}
|
||||
|
||||
print("Creating some slices")
|
||||
slices = [
|
||||
Slice(
|
||||
slice_name="Girls",
|
||||
viz_type='table',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
groupby=['name'],
|
||||
flt_col_1='gender',
|
||||
flt_eq_1="girl", row_limit=50)),
|
||||
Slice(
|
||||
slice_name="Boys",
|
||||
viz_type='table',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
groupby=['name'],
|
||||
flt_col_1='gender',
|
||||
flt_eq_1="boy",
|
||||
row_limit=50)),
|
||||
Slice(
|
||||
slice_name="Participants",
|
||||
viz_type='big_number',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="big_number", granularity="ds",
|
||||
compare_lag="5", compare_suffix="over 5Y")),
|
||||
Slice(
|
||||
slice_name="Number of Girls",
|
||||
viz_type='big_number_total',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="big_number_total", granularity="ds",
|
||||
flt_col_1='gender', flt_eq_1='girl',
|
||||
subheader='total female participants')),
|
||||
Slice(
|
||||
slice_name="Genders",
|
||||
viz_type='pie',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="pie", groupby=['gender'])),
|
||||
Slice(
|
||||
slice_name="Genders by State",
|
||||
viz_type='dist_bar',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
flt_eq_1="other", viz_type="dist_bar",
|
||||
metrics=['sum__sum_girls', 'sum__sum_boys'],
|
||||
groupby=['state'], flt_op_1='not in', flt_col_1='state')),
|
||||
Slice(
|
||||
slice_name="Trends",
|
||||
viz_type='line',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="line", groupby=['name'],
|
||||
granularity='ds', rich_tooltip='y', show_legend='y')),
|
||||
Slice(
|
||||
slice_name="Title",
|
||||
viz_type='markup',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="markup", markup_type="html",
|
||||
code="""\
|
||||
<div style="text-align:center">
|
||||
<h1>Birth Names Dashboard</h1>
|
||||
<p>
|
||||
The source dataset came from
|
||||
<a href="https://github.com/hadley/babynames">[here]</a>
|
||||
</p>
|
||||
<img src="http://monblog.system-linux.net/image/tux/baby-tux_overlord59-tux.png">
|
||||
</div>
|
||||
""")),
|
||||
Slice(
|
||||
slice_name="Name Cloud",
|
||||
viz_type='word_cloud',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="word_cloud", size_from="10",
|
||||
series='name', size_to="70", rotation="square",
|
||||
limit='100')),
|
||||
Slice(
|
||||
slice_name="Pivot Table",
|
||||
viz_type='pivot_table',
|
||||
datasource_type='table',
|
||||
table=tbl,
|
||||
params=get_slice_json(
|
||||
defaults,
|
||||
viz_type="pivot_table", metrics=['sum__num'],
|
||||
groupby=['name'], columns=['state'])),
|
||||
]
|
||||
for slc in slices:
|
||||
merge_slice(slc)
|
||||
|
||||
print("Creating a dashboard")
|
||||
dash = db.session.query(Dash).filter_by(dashboard_title="Births").first()
|
||||
|
||||
if not dash:
|
||||
dash = Dash()
|
||||
js = textwrap.dedent("""\
|
||||
[
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 2,
|
||||
"col": 8,
|
||||
"slice_id": "85",
|
||||
"row": 7
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 2,
|
||||
"col": 10,
|
||||
"slice_id": "86",
|
||||
"row": 7
|
||||
},
|
||||
{
|
||||
"size_y": 2,
|
||||
"size_x": 2,
|
||||
"col": 1,
|
||||
"slice_id": "87",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 2,
|
||||
"size_x": 2,
|
||||
"col": 3,
|
||||
"slice_id": "88",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 7,
|
||||
"col": 5,
|
||||
"slice_id": "89",
|
||||
"row": 4
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 7,
|
||||
"col": 1,
|
||||
"slice_id": "90",
|
||||
"row": 7
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 3,
|
||||
"col": 9,
|
||||
"slice_id": "91",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 3,
|
||||
"size_x": 4,
|
||||
"col": 5,
|
||||
"slice_id": "92",
|
||||
"row": 1
|
||||
},
|
||||
{
|
||||
"size_y": 4,
|
||||
"size_x": 4,
|
||||
"col": 1,
|
||||
"slice_id": "93",
|
||||
"row": 3
|
||||
}
|
||||
]
|
||||
""")
|
||||
l = json.loads(js)
|
||||
for i, pos in enumerate(l):
|
||||
pos['slice_id'] = str(slices[i].id)
|
||||
dash.dashboard_title = "Births"
|
||||
dash.position_json = json.dumps(l, indent=4)
|
||||
dash.slug = "births"
|
||||
dash.slices = slices
|
||||
db.session.merge(dash)
|
||||
db.session.commit()
|
||||
631
caravel/forms.py
@@ -1,631 +0,0 @@
|
||||
"""Contains the logic to create cohesive forms on the explore view"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict
|
||||
from copy import copy
|
||||
|
||||
from wtforms import (
|
||||
Form, SelectMultipleField, SelectField, TextField, TextAreaField,
|
||||
BooleanField, IntegerField, HiddenField)
|
||||
from wtforms import validators, widgets
|
||||
|
||||
from caravel import app
|
||||
|
||||
config = app.config
|
||||
|
||||
|
||||
class BetterBooleanField(BooleanField):
|
||||
|
||||
"""Fixes the html checkbox to distinguish absent from unchecked
|
||||
|
||||
(which doesn't distinguish False from NULL/missing )
|
||||
If value is unchecked, this hidden <input> fills in False value
|
||||
"""
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
html = super(BetterBooleanField, self).__call__(**kwargs)
|
||||
html += u'<input type="hidden" name="{}" value="false">'.format(self.name)
|
||||
return widgets.HTMLString(html)
|
||||
|
||||
|
||||
class SelectMultipleSortableField(SelectMultipleField):
|
||||
|
||||
"""Works along with select2sortable to preserves the sort order"""
|
||||
|
||||
def iter_choices(self):
|
||||
d = OrderedDict()
|
||||
for value, label in self.choices:
|
||||
selected = self.data is not None and self.coerce(value) in self.data
|
||||
d[value] = (value, label, selected)
|
||||
if self.data:
|
||||
for value in self.data:
|
||||
if value:
|
||||
yield d.pop(value)
|
||||
while d:
|
||||
yield d.popitem(last=False)[1]
|
||||
|
||||
|
||||
class FreeFormSelect(widgets.Select):
|
||||
|
||||
"""A WTF widget that allows for free form entry"""
|
||||
|
||||
def __call__(self, field, **kwargs):
|
||||
kwargs.setdefault('id', field.id)
|
||||
if self.multiple:
|
||||
kwargs['multiple'] = True
|
||||
html = ['<select %s>' % widgets.html_params(name=field.name, **kwargs)]
|
||||
found = False
|
||||
for val, label, selected in field.iter_choices():
|
||||
html.append(self.render_option(val, label, selected))
|
||||
if field.data and val == field.data:
|
||||
found = True
|
||||
if not found:
|
||||
html.insert(1, self.render_option(field.data, field.data, True))
|
||||
html.append('</select>')
|
||||
return widgets.HTMLString(''.join(html))
|
||||
|
||||
|
||||
class FreeFormSelectField(SelectField):
|
||||
|
||||
"""A WTF SelectField that allows for free form input"""
|
||||
|
||||
widget = FreeFormSelect()
|
||||
|
||||
def pre_validate(self, form):
|
||||
return
|
||||
|
||||
|
||||
class OmgWtForm(Form):
|
||||
|
||||
"""Caravelification of the WTForm Form object"""
|
||||
|
||||
fieldsets = {}
|
||||
css_classes = dict()
|
||||
|
||||
def get_field(self, fieldname):
|
||||
return getattr(self, fieldname)
|
||||
|
||||
def field_css_classes(self, fieldname):
|
||||
print(fieldname, self.css_classes[fieldname])
|
||||
if fieldname in self.css_classes:
|
||||
return " ".join(self.css_classes[fieldname])
|
||||
return ""
|
||||
|
||||
|
||||
class FormFactory(object):
|
||||
|
||||
"""Used to create the forms in the explore view dynamically"""
|
||||
|
||||
series_limits = [0, 5, 10, 25, 50, 100, 500]
|
||||
fieltype_class = {
|
||||
SelectField: 'select2',
|
||||
SelectMultipleField: 'select2',
|
||||
FreeFormSelectField: 'select2_freeform',
|
||||
SelectMultipleSortableField: 'select2Sortable',
|
||||
}
|
||||
|
||||
def __init__(self, viz):
|
||||
self.viz = viz
|
||||
from caravel.viz import viz_types
|
||||
viz = self.viz
|
||||
datasource = viz.datasource
|
||||
default_metric = datasource.metrics_combo[0][0]
|
||||
|
||||
gb_cols = datasource.groupby_column_names
|
||||
default_groupby = gb_cols[0] if gb_cols else None
|
||||
group_by_choices = [(s, s) for s in datasource.groupby_column_names]
|
||||
# Pool of all the fields that can be used in Caravel
|
||||
self.field_dict = {
|
||||
'viz_type': SelectField(
|
||||
'Viz',
|
||||
default='table',
|
||||
choices=[(k, v.verbose_name) for k, v in viz_types.items()],
|
||||
description="The type of visualization to display"),
|
||||
'metrics': SelectMultipleSortableField(
|
||||
'Metrics', choices=datasource.metrics_combo,
|
||||
default=[default_metric],
|
||||
description="One or many metrics to display"),
|
||||
'metric': SelectField(
|
||||
'Metric', choices=datasource.metrics_combo,
|
||||
default=default_metric,
|
||||
description="Chose the metric"),
|
||||
'stacked_style': SelectField(
|
||||
'Chart Style', choices=self.choicify(
|
||||
['stack', 'stream', 'expand']),
|
||||
default='stack',
|
||||
description=""),
|
||||
'linear_color_scheme': SelectField(
|
||||
'Color Scheme', choices=self.choicify([
|
||||
'fire', 'blue_white_yellow', 'white_black',
|
||||
'black_white']),
|
||||
default='blue_white_yellow',
|
||||
description=""),
|
||||
'normalize_across': SelectField(
|
||||
'Normalize Across', choices=self.choicify([
|
||||
'heatmap', 'x', 'y']),
|
||||
default='heatmap',
|
||||
description=(
|
||||
"Color will be rendered based on a ratio "
|
||||
"of the cell against the sum of across this "
|
||||
"criteria")),
|
||||
'canvas_image_rendering': SelectField(
|
||||
'Rendering', choices=(
|
||||
('pixelated', 'pixelated (Sharp)'),
|
||||
('auto', 'auto (Smooth)'),
|
||||
),
|
||||
default='pixelated',
|
||||
description=(
|
||||
"image-rendering CSS attribute of the canvas object that "
|
||||
"defines how the browser scales up the image")),
|
||||
'xscale_interval': SelectField(
|
||||
'XScale Interval', choices=self.choicify(range(1, 50)),
|
||||
default='1',
|
||||
description=(
|
||||
"Number of step to take between ticks when "
|
||||
"printing the x scale")),
|
||||
'yscale_interval': SelectField(
|
||||
'YScale Interval', choices=self.choicify(range(1, 50)),
|
||||
default='1',
|
||||
description=(
|
||||
"Number of step to take between ticks when "
|
||||
"printing the y scale")),
|
||||
'bar_stacked': BetterBooleanField(
|
||||
'Stacked Bars',
|
||||
default=False,
|
||||
description=""),
|
||||
'secondary_metric': SelectField(
|
||||
'Color Metric', choices=datasource.metrics_combo,
|
||||
default=default_metric,
|
||||
description="A metric to use for color"),
|
||||
'country_fieldtype': SelectField(
|
||||
'Country Field Type',
|
||||
default='cca2',
|
||||
choices=(
|
||||
('name', 'Full name'),
|
||||
('cioc', 'code International Olympic Committee (cioc)'),
|
||||
('cca2', 'code ISO 3166-1 alpha-2 (cca2)'),
|
||||
('cca3', 'code ISO 3166-1 alpha-3 (cca3)'),
|
||||
),
|
||||
description=(
|
||||
"The country code standard that Caravel should expect "
|
||||
"to find in the [country] column")),
|
||||
'groupby': SelectMultipleSortableField(
|
||||
'Group by',
|
||||
choices=self.choicify(datasource.groupby_column_names),
|
||||
description="One or many fields to group by"),
|
||||
'columns': SelectMultipleSortableField(
|
||||
'Columns',
|
||||
choices=self.choicify(datasource.groupby_column_names),
|
||||
description="One or many fields to pivot as columns"),
|
||||
'all_columns': SelectMultipleSortableField(
|
||||
'Columns',
|
||||
choices=self.choicify(datasource.column_names),
|
||||
description="Columns to display"),
|
||||
'all_columns_x': SelectField(
|
||||
'X',
|
||||
choices=self.choicify(datasource.column_names),
|
||||
description="Columns to display"),
|
||||
'all_columns_y': SelectField(
|
||||
'Y',
|
||||
choices=self.choicify(datasource.column_names),
|
||||
description="Columns to display"),
|
||||
'granularity': FreeFormSelectField(
|
||||
'Time Granularity', default="one day",
|
||||
choices=self.choicify([
|
||||
'all',
|
||||
'5 seconds',
|
||||
'30 seconds',
|
||||
'1 minute',
|
||||
'5 minutes',
|
||||
'1 hour',
|
||||
'6 hour',
|
||||
'1 day',
|
||||
'7 days',
|
||||
]),
|
||||
description=(
|
||||
"The time granularity for the visualization. Note that you "
|
||||
"can type and use simple natural language as in '10 seconds', "
|
||||
"'1 day' or '56 weeks'")),
|
||||
'link_length': FreeFormSelectField(
|
||||
'Link Length', default="200",
|
||||
choices=self.choicify([
|
||||
'10',
|
||||
'25',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
'150',
|
||||
'200',
|
||||
'250',
|
||||
]),
|
||||
description="Link length in the force layout"),
|
||||
'charge': FreeFormSelectField(
|
||||
'Charge', default="-500",
|
||||
choices=self.choicify([
|
||||
'-50',
|
||||
'-75',
|
||||
'-100',
|
||||
'-150',
|
||||
'-200',
|
||||
'-250',
|
||||
'-500',
|
||||
'-1000',
|
||||
'-2500',
|
||||
'-5000',
|
||||
]),
|
||||
description="Charge in the force layout"),
|
||||
'granularity_sqla': SelectField(
|
||||
'Time Column',
|
||||
default=datasource.main_dttm_col or datasource.any_dttm_col,
|
||||
choices=self.choicify(datasource.dttm_cols),
|
||||
description=(
|
||||
"The time column for the visualization. Note that you "
|
||||
"can define arbitrary expression that return a DATETIME "
|
||||
"column in the table editor. Also note that the "
|
||||
"filter bellow is applied against this column or "
|
||||
"expression")),
|
||||
'resample_rule': FreeFormSelectField(
|
||||
'Resample Rule', default='',
|
||||
choices=self.choicify(('1T', '1H', '1D', '7D', '1M', '1AS')),
|
||||
description=("Pandas resample rule")),
|
||||
'resample_how': FreeFormSelectField(
|
||||
'Resample How', default='',
|
||||
choices=self.choicify(('', 'mean', 'sum', 'median')),
|
||||
description=("Pandas resample how")),
|
||||
'resample_fillmethod': FreeFormSelectField(
|
||||
'Resample Fill Method', default='',
|
||||
choices=self.choicify(('', 'ffill', 'bfill')),
|
||||
description=("Pandas resample fill method")),
|
||||
'since': FreeFormSelectField(
|
||||
'Since', default="7 days ago",
|
||||
choices=self.choicify([
|
||||
'1 hour ago',
|
||||
'12 hours ago',
|
||||
'1 day ago',
|
||||
'7 days ago',
|
||||
'28 days ago',
|
||||
'90 days ago',
|
||||
'1 year ago'
|
||||
]),
|
||||
description=(
|
||||
"Timestamp from filter. This supports free form typing and "
|
||||
"natural language as in '1 day ago', '28 days' or '3 years'")),
|
||||
'until': FreeFormSelectField(
|
||||
'Until', default="now",
|
||||
choices=self.choicify([
|
||||
'now',
|
||||
'1 day ago',
|
||||
'7 days ago',
|
||||
'28 days ago',
|
||||
'90 days ago',
|
||||
'1 year ago'])
|
||||
),
|
||||
'max_bubble_size': FreeFormSelectField(
|
||||
'Max Bubble Size', default="25",
|
||||
choices=self.choicify([
|
||||
'5',
|
||||
'10',
|
||||
'15',
|
||||
'25',
|
||||
'50',
|
||||
'75',
|
||||
'100',
|
||||
])
|
||||
),
|
||||
'whisker_options': FreeFormSelectField(
|
||||
'Whisker/outlier options', default="Tukey",
|
||||
description=(
|
||||
"Determines how whiskers and outliers are calculated."),
|
||||
choices=self.choicify([
|
||||
'Tukey',
|
||||
'Min/max (no outliers)',
|
||||
'2/98 percentiles',
|
||||
'9/91 percentiles',
|
||||
])
|
||||
),
|
||||
'row_limit':
|
||||
FreeFormSelectField(
|
||||
'Row limit',
|
||||
default=config.get("ROW_LIMIT"),
|
||||
choices=self.choicify(
|
||||
[10, 50, 100, 250, 500, 1000, 5000, 10000, 50000])),
|
||||
'limit':
|
||||
FreeFormSelectField(
|
||||
'Series limit',
|
||||
choices=self.choicify(self.series_limits),
|
||||
default=50,
|
||||
description=(
|
||||
"Limits the number of time series that get displayed")),
|
||||
'rolling_type': SelectField(
|
||||
'Rolling',
|
||||
default='None',
|
||||
choices=[(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']],
|
||||
description=(
|
||||
"Defines a rolling window function to apply, works along "
|
||||
"with the [Periods] text box")),
|
||||
'rolling_periods': IntegerField(
|
||||
'Periods',
|
||||
validators=[validators.optional()],
|
||||
description=(
|
||||
"Defines the size of the rolling window function, "
|
||||
"relative to the time granularity selected")),
|
||||
'series': SelectField(
|
||||
'Series', choices=group_by_choices,
|
||||
default=default_groupby,
|
||||
description=(
|
||||
"Defines the grouping of entities. "
|
||||
"Each serie is shown as a specific color on the chart and "
|
||||
"has a legend toggle")),
|
||||
'entity': SelectField(
|
||||
'Entity', choices=group_by_choices,
|
||||
default=default_groupby,
|
||||
description="This define the element to be plotted on the chart"),
|
||||
'x': SelectField(
|
||||
'X Axis', choices=datasource.metrics_combo,
|
||||
default=default_metric,
|
||||
description="Metric assigned to the [X] axis"),
|
||||
'y': SelectField(
|
||||
'Y Axis', choices=datasource.metrics_combo,
|
||||
default=default_metric,
|
||||
description="Metric assigned to the [Y] axis"),
|
||||
'size': SelectField(
|
||||
'Bubble Size',
|
||||
default=default_metric,
|
||||
choices=datasource.metrics_combo),
|
||||
'url': TextField(
|
||||
'URL', default='www.airbnb.com',),
|
||||
'where': TextField(
|
||||
'Custom WHERE clause', default='',
|
||||
description=(
|
||||
"The text in this box gets included in your query's WHERE "
|
||||
"clause, as an AND to other criteria. You can include "
|
||||
"complex expression, parenthesis and anything else "
|
||||
"supported by the backend it is directed towards.")),
|
||||
'having': TextField(
|
||||
'Custom HAVING clause', default='',
|
||||
description=(
|
||||
"The text in this box gets included in your query's HAVING"
|
||||
" clause, as an AND to other criteria. You can include "
|
||||
"complex expression, parenthesis and anything else "
|
||||
"supported by the backend it is directed towards.")),
|
||||
'compare_lag': TextField(
|
||||
'Comparison Period Lag',
|
||||
description=(
|
||||
"Based on granularity, number of time periods to "
|
||||
"compare against")),
|
||||
'compare_suffix': TextField(
|
||||
'Comparison suffix',
|
||||
description="Suffix to apply after the percentage display"),
|
||||
'x_axis_format': FreeFormSelectField(
|
||||
'X axis format',
|
||||
default='smart_date',
|
||||
choices=[
|
||||
('smart_date', 'Adaptative formating'),
|
||||
("%m/%d/%Y", '"%m/%d/%Y" | 01/14/2019'),
|
||||
("%Y-%m-%d", '"%Y-%m-%d" | 2019-01-14'),
|
||||
("%Y-%m-%d %H:%M:%S",
|
||||
'"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'),
|
||||
("%H:%M:%S", '"%H:%M:%S" | 01:32:10'),
|
||||
],
|
||||
description="D3 format syntax for y axis "
|
||||
"https://github.com/mbostock/\n"
|
||||
"d3/wiki/Formatting"),
|
||||
'y_axis_format': FreeFormSelectField(
|
||||
'Y axis format',
|
||||
default='.3s',
|
||||
choices=[
|
||||
('.3s', '".3s" | 12.3k'),
|
||||
('.3%', '".3%" | 1234543.210%'),
|
||||
('.4r', '".4r" | 12350'),
|
||||
('.3f', '".3f" | 12345.432'),
|
||||
('+,', '"+," | +12,345.4321'),
|
||||
('$,.2f', '"$,.2f" | $12,345.43'),
|
||||
],
|
||||
description="D3 format syntax for y axis "
|
||||
"https://github.com/mbostock/\n"
|
||||
"d3/wiki/Formatting"),
|
||||
'markup_type': SelectField(
|
||||
"Markup Type",
|
||||
choices=self.choicify(['markdown', 'html']),
|
||||
default="markdown",
|
||||
description="Pick your favorite markup language"),
|
||||
'rotation': SelectField(
|
||||
"Rotation",
|
||||
choices=[(s, s) for s in ['random', 'flat', 'square']],
|
||||
default="random",
|
||||
description="Rotation to apply to words in the cloud"),
|
||||
'line_interpolation': SelectField(
|
||||
"Line Style",
|
||||
choices=self.choicify([
|
||||
'linear', 'basis', 'cardinal', 'monotone',
|
||||
'step-before', 'step-after']),
|
||||
default='linear',
|
||||
description="Line interpolation as defined by d3.js"),
|
||||
'code': TextAreaField(
|
||||
"Code", description="Put your code here", default=''),
|
||||
'pandas_aggfunc': SelectField(
|
||||
"Aggregation function",
|
||||
choices=self.choicify([
|
||||
'sum', 'mean', 'min', 'max', 'median', 'stdev', 'var']),
|
||||
default='sum',
|
||||
description=(
|
||||
"Aggregate function to apply when pivoting and "
|
||||
"computing the total rows and columns")),
|
||||
'size_from': TextField(
|
||||
"Font Size From",
|
||||
default="20",
|
||||
description="Font size for the smallest value in the list"),
|
||||
'size_to': TextField(
|
||||
"Font Size To",
|
||||
default="150",
|
||||
description="Font size for the biggest value in the list"),
|
||||
'show_brush': BetterBooleanField(
|
||||
"Range Filter", default=False,
|
||||
description=(
|
||||
"Whether to display the time range interactive selector")),
|
||||
'show_datatable': BetterBooleanField(
|
||||
"Data Table", default=False,
|
||||
description="Whether to display the interactive data table"),
|
||||
'include_search': BetterBooleanField(
|
||||
"Search Box", default=False,
|
||||
description=(
|
||||
"Whether to include a client side search box")),
|
||||
'show_bubbles': BetterBooleanField(
|
||||
"Show Bubbles", default=False,
|
||||
description=(
|
||||
"Whether to display bubbles on top of countries")),
|
||||
'show_legend': BetterBooleanField(
|
||||
"Legend", default=True,
|
||||
description="Whether to display the legend (toggles)"),
|
||||
'x_axis_showminmax': BetterBooleanField(
|
||||
"X bounds", default=True,
|
||||
description=(
|
||||
"Whether to display the min and max values of the X axis")),
|
||||
'rich_tooltip': BetterBooleanField(
|
||||
"Rich Tooltip", default=True,
|
||||
description=(
|
||||
"The rich tooltip shows a list of all series for that"
|
||||
" point in time")),
|
||||
'y_axis_zero': BetterBooleanField(
|
||||
"Y Axis Zero", default=False,
|
||||
description=(
|
||||
"Force the Y axis to start at 0 instead of the minimum "
|
||||
"value")),
|
||||
'y_log_scale': BetterBooleanField(
|
||||
"Y Log", default=False,
|
||||
description="Use a log scale for the Y axis"),
|
||||
'x_log_scale': BetterBooleanField(
|
||||
"X Log", default=False,
|
||||
description="Use a log scale for the X axis"),
|
||||
'donut': BetterBooleanField(
|
||||
"Donut", default=False,
|
||||
description="Do you want a donut or a pie?"),
|
||||
'contribution': BetterBooleanField(
|
||||
"Contribution", default=False,
|
||||
description="Compute the contribution to the total"),
|
||||
'num_period_compare': IntegerField(
|
||||
"Period Ratio", default=None,
|
||||
validators=[validators.optional()],
|
||||
description=(
|
||||
"[integer] Number of period to compare against, "
|
||||
"this is relative to the granularity selected")),
|
||||
'time_compare': TextField(
|
||||
"Time Shift",
|
||||
default="",
|
||||
description=(
|
||||
"Overlay a timeseries from a "
|
||||
"relative time period. Expects relative time delta "
|
||||
"in natural language (example: 24 hours, 7 days, "
|
||||
"56 weeks, 365 days")),
|
||||
'subheader': TextField(
|
||||
'Subheader',
|
||||
description=(
|
||||
"Description text that shows up below your Big "
|
||||
"Number")),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def choicify(l):
|
||||
return [("{}".format(obj), "{}".format(obj)) for obj in l]
|
||||
|
||||
def get_form(self):
|
||||
"""Returns a form object based on the viz/datasource/context"""
|
||||
viz = self.viz
|
||||
field_css_classes = {}
|
||||
for name, obj in self.field_dict.items():
|
||||
field_css_classes[name] = ['form-control']
|
||||
s = self.fieltype_class.get(obj.field_class)
|
||||
if s:
|
||||
field_css_classes[name] += [s]
|
||||
|
||||
for field in ('show_brush', 'show_legend', 'rich_tooltip'):
|
||||
field_css_classes[field] += ['input-sm']
|
||||
|
||||
class QueryForm(OmgWtForm):
|
||||
|
||||
"""The dynamic form object used for the explore view"""
|
||||
|
||||
fieldsets = copy(viz.fieldsets)
|
||||
css_classes = field_css_classes
|
||||
standalone = HiddenField()
|
||||
async = HiddenField()
|
||||
force = HiddenField()
|
||||
extra_filters = HiddenField()
|
||||
json = HiddenField()
|
||||
slice_id = HiddenField()
|
||||
slice_name = HiddenField()
|
||||
previous_viz_type = HiddenField(default=viz.viz_type)
|
||||
collapsed_fieldsets = HiddenField()
|
||||
viz_type = self.field_dict.get('viz_type')
|
||||
|
||||
filter_cols = viz.datasource.filterable_column_names or ['']
|
||||
for i in range(10):
|
||||
setattr(QueryForm, 'flt_col_' + str(i), SelectField(
|
||||
'Filter 1',
|
||||
default=filter_cols[0],
|
||||
choices=self.choicify(filter_cols)))
|
||||
setattr(QueryForm, 'flt_op_' + str(i), SelectField(
|
||||
'Filter 1',
|
||||
default='in',
|
||||
choices=self.choicify(['in', 'not in'])))
|
||||
setattr(
|
||||
QueryForm, 'flt_eq_' + str(i),
|
||||
TextField("Super", default=''))
|
||||
|
||||
for field in viz.flat_form_fields():
|
||||
setattr(QueryForm, field, self.field_dict[field])
|
||||
|
||||
def add_to_form(attrs):
|
||||
for attr in attrs:
|
||||
setattr(QueryForm, attr, self.field_dict[attr])
|
||||
|
||||
# datasource type specific form elements
|
||||
if viz.datasource.__class__.__name__ == 'SqlaTable':
|
||||
QueryForm.fieldsets += ({
|
||||
'label': 'SQL',
|
||||
'fields': ['where', 'having'],
|
||||
'description': (
|
||||
"This section exposes ways to include snippets of "
|
||||
"SQL in your query"),
|
||||
},)
|
||||
add_to_form(('where', 'having'))
|
||||
grains = viz.datasource.database.grains()
|
||||
|
||||
if not viz.datasource.any_dttm_col:
|
||||
return QueryForm
|
||||
if grains:
|
||||
time_fields = ('granularity_sqla', 'time_grain_sqla')
|
||||
self.field_dict['time_grain_sqla'] = SelectField(
|
||||
'Time Grain',
|
||||
choices=self.choicify((grain.name for grain in grains)),
|
||||
default="Time Column",
|
||||
description=(
|
||||
"The time granularity for the visualization. This "
|
||||
"applies a date transformation to alter "
|
||||
"your time column and defines a new time granularity."
|
||||
"The options here are defined on a per database "
|
||||
"engine basis in the Caravel source code"))
|
||||
add_to_form(time_fields)
|
||||
field_css_classes['time_grain_sqla'] = ['form-control', 'select2']
|
||||
field_css_classes['granularity_sqla'] = ['form-control', 'select2']
|
||||
else:
|
||||
time_fields = 'granularity_sqla'
|
||||
add_to_form((time_fields, ))
|
||||
else:
|
||||
time_fields = 'granularity'
|
||||
add_to_form(('granularity',))
|
||||
field_css_classes['granularity'] = ['form-control', 'select2_freeform']
|
||||
add_to_form(('since', 'until'))
|
||||
|
||||
QueryForm.fieldsets = ({
|
||||
'label': 'Time',
|
||||
'fields': (
|
||||
time_fields,
|
||||
('since', 'until'),
|
||||
),
|
||||
'description': "Time related form attributes",
|
||||
},) + tuple(QueryForm.fieldsets)
|
||||
return QueryForm
|
||||
1305
caravel/models.py
@@ -1,31 +0,0 @@
|
||||
{% set menu = appbuilder.menu %}
|
||||
{% set languages = appbuilder.languages %}
|
||||
|
||||
<div class="navbar navbar-static-top {{menu.extra_classes}}" role="navigation">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" style="padding:7px;opacity:0.9;" href="{{appbuilder.get_url_for_index}}">
|
||||
<img width="50" src="/static/assets/images/caravel_logo.png">
|
||||
</a>
|
||||
<span class="navbar-brand">
|
||||
<a href="{{appbuilder.get_url_for_index}}">
|
||||
{{ appbuilder.app_name }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
{% include 'appbuilder/navbar_menu.html' %}
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="http://airbnb.io/caravel"><i class="fa fa-book"></i> Documentation</a></li>
|
||||
{% include 'appbuilder/navbar_right.html' %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +0,0 @@
|
||||
<script>
|
||||
var msg = "Click on a table link to create a Slice";
|
||||
window.location = "/r/msg/?url={{ '/tablemodelview/list/' }}&msg=" + msg;
|
||||
</script>
|
||||
@@ -1,12 +0,0 @@
|
||||
{% extends "appbuilder/baselayout.html" %}
|
||||
|
||||
{% block head_css %}
|
||||
<link rel="icon" type="image/png" href="/static/assets/images/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/caravel.css" />
|
||||
{{super()}}
|
||||
{% endblock %}
|
||||
|
||||
{% block head_js %}
|
||||
{{super()}}
|
||||
<script src="/static/assets/javascripts/dist/css-theme.entry.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,139 +0,0 @@
|
||||
{% extends "caravel/basic.html" %}
|
||||
|
||||
{% block head_js %}
|
||||
{{ super() }}
|
||||
<script src="/static/assets/javascripts/dist/dashboard.entry.js"></script>
|
||||
{% endblock %}
|
||||
{% block title %}[dashboard] {{ dashboard.dashboard_title }}{% endblock %}
|
||||
{% block body %}
|
||||
<div class="dashboard container-fluid" data-dashboard="{{ dashboard.json_data }}" data-css="{{ dashboard.css }}">
|
||||
|
||||
<!-- Modal -->
|
||||
<div class="modal fade" id="css_modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content css">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="myModalLabel">Dashboard CSS</h4>
|
||||
<h6><strong>Styling applies to this dashboard only</strong></h6>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<select id="css_template" class="select2" style="margin-bottom: 5px;">
|
||||
<option value="" data-css="">CSS template</option>
|
||||
{% for t in templates %}
|
||||
<option value="{{ t.id }}" data-css="{{t.css}}">
|
||||
{{ t.template_name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select><br>
|
||||
<textarea id="dash_css" rows="30" cols="60">{{ dashboard.css }}</textarea>
|
||||
<input type="hidden" id="dashboard_id" value="{{ dashboard.id }}" />
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title">
|
||||
<div class="row">
|
||||
<div class="col-md-3"></div>
|
||||
<div class="col-md-6">
|
||||
<h2>
|
||||
<span class="favstar" class_name="Dashboard" obj_id="{{ dashboard.id }}"></span>
|
||||
{{ dashboard.dashboard_title }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="btn-group pull-right" role="group" >
|
||||
<button type="button" id="refresh_dash" class="btn btn-default" data-toggle="tooltip" title="Force refresh the whole dashboard">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</button>
|
||||
<button type="button" id="filters" class="btn btn-default" data-toggle="tooltip" title="View the list of active filters">
|
||||
<i class="fa fa-filter"></i>
|
||||
</button>
|
||||
<button type="button" id="css" class="btn btn-default" data-toggle="modal" data-target="#css_modal">
|
||||
<i class="fa fa-css3" data-toggle="tooltip" title="Edit the dashboard's CSS"></i>
|
||||
</button>
|
||||
<a id="editdash" class="btn btn-default" href="/dashboardmodelview/edit/{{ dashboard.id }}" title="Edit this dashboard's property" data-toggle="tooltip" >
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" id="savedash" class="btn btn-default" data-toggle="tooltip" title="Save the current positioning and CSS">
|
||||
<i class="fa fa-save"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gridster content_fluid" style="visibility: hidden;">
|
||||
<ul>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
||||
|
||||
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
slice_id="{{ slice.id }}"
|
||||
class="widget {{ slice.viz_type }}"
|
||||
data-row="{{ pos.row or 1 }}"
|
||||
data-col="{{ pos.col or loop.index }}"
|
||||
data-sizex="{{ pos.size_x or 4 }}"
|
||||
data-sizey="{{ pos.size_y or 4 }}">
|
||||
|
||||
<div class="chart-header">
|
||||
<div class="row">
|
||||
<div class="col-md-12 text-center header">
|
||||
{{ slice.slice_name }}
|
||||
</div>
|
||||
<div class="col-md-12 chart-controls">
|
||||
<div class="pull-left">
|
||||
<a title="Move chart" data-toggle="tooltip">
|
||||
<i class="fa fa-arrows drag"></i>
|
||||
</a>
|
||||
<a class="refresh" title="Force refresh data" data-toggle="tooltip">
|
||||
<i class="fa fa-repeat"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
{% if slice.description %}
|
||||
<a title="Toggle chart description">
|
||||
<i class="fa fa-info-circle slice_info" slice_id="{{ slice.id }}" title="{{ slice.description }}" data-toggle="tooltip"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{{ slice.edit_url }}" title="Edit chart" data-toggle="tooltip">
|
||||
<i class="fa fa-pencil"></i>
|
||||
</a>
|
||||
<a href="{{ slice.slice_url }}" title="Explore chart" data-toggle="tooltip">
|
||||
<i class="fa fa-share"></i>
|
||||
</a>
|
||||
<a class="remove-chart" title="Remove chart from dashboard" data-toggle="tooltip">
|
||||
<i class="fa fa-close"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="slice_description bs-callout bs-callout-default"
|
||||
style="{{ 'display: none;' if "{}".format(slice.id) not in dashboard.metadata_dejson.expanded_slices }}">
|
||||
{{ slice.description_markeddown | safe }}
|
||||
</div>
|
||||
<div class="row chart-container">
|
||||
<input type="hidden" slice_id="{{ slice.id }}" value="false">
|
||||
<div id="{{ slice.token }}" class="token col-md-12">
|
||||
<img src="{{ url_for("static", filename="assets/images/loading.gif") }}" class="loading" alt="loading">
|
||||
<div class="slice_container" id="{{ slice.token }}_con"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,228 +0,0 @@
|
||||
{% extends "caravel/basic.html" %}
|
||||
|
||||
{% block title %}
|
||||
{% if slice %}
|
||||
[slice] {{ slice.slice_name }}
|
||||
{% else %}
|
||||
[explore] {{ viz.datasource.table_name }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
{% set datasource = viz.datasource %}
|
||||
{% set form = viz.form %}
|
||||
|
||||
{% macro panofield(fieldname)%}
|
||||
<div>
|
||||
{% set field = form.get_field(fieldname)%}
|
||||
<div>
|
||||
{{ viz.get_form_override(fieldname, 'label') or field.label }}
|
||||
{% if field.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="right"
|
||||
title="{{ viz.get_form_override(fieldname, 'description') or field.description }}"></i>
|
||||
{% endif %}
|
||||
{{ field(class_=form.field_css_classes(field.name)) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="datasource container-fluid">
|
||||
<form id="query" method="GET" style="display: none;">
|
||||
<div class="header">
|
||||
<span title="Data Source" data-toggle="tooltip">
|
||||
<select id="datasource_id" class="select2">
|
||||
{% for ds in datasources %}
|
||||
<option url="{{ ds.explore_url }}" {{ "selected" if ds.id == datasource.id }} value="{{ ds.id }}">{{ ds.full_name }}<i class="fa fa-info"></i></option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</span>
|
||||
<a href="{{ datasource.url }}" class="btn btn-default-outline" data-toggle="tooltip" title="Edit/configure datasource">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
<span title="Visualization Type" data-toggle="tooltip">
|
||||
{{ form.get_field("viz_type")(class_="select2-with-images") }}
|
||||
</span>
|
||||
{% if slice %}
|
||||
<span class="btn btn-default notbtn" title="Slice" data-toggle="tooltip" data-placement="bottom">
|
||||
<span class="favstar" class_name="Slice" obj_id="{{ slice.id }}"></span>
|
||||
{{ slice.slice_name }}
|
||||
<a class="" href="/slicemodelview/edit/{{ slice.id }}" data-toggle="tooltip" title="Edit">
|
||||
{% if slice.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ slice.description }}"></i>
|
||||
{% endif %}
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
</span>
|
||||
{% endif %}
|
||||
<div class="pull-right">
|
||||
<span id="is_cached" class="label label-default" title="Force refresh" data-toggle="tooltip">
|
||||
cached
|
||||
</span>
|
||||
<div class="btn-group results" role="group">
|
||||
<a role="button" tabindex="0" class="btn btn-default" id="shortner" data-toggle="popover" data-trigger="focus">
|
||||
<i class="fa fa-link" data-toggle="tooltip" title="Short URL"></i>
|
||||
</a>
|
||||
<span class="btn btn-default" id="standalone" title="Standalone version, use to embed anywhere" data-toggle="tooltip">
|
||||
<i class="fa fa-code"></i>
|
||||
</span>
|
||||
<span class="btn btn-default " id="json" title="Export to .json" data-toggle="tooltip">
|
||||
<i class="fa fa-file-code-o"></i>
|
||||
.json
|
||||
</span>
|
||||
<span class="btn btn-default " id="csv" title="Export to .csv format" data-toggle="tooltip">
|
||||
<i class="fa fa-file-text-o"></i>.csv
|
||||
</span>
|
||||
<span class="btn btn-warning notbtn" id="timer">0 sec</span>
|
||||
<span class="btn btn-info disabled query"
|
||||
data-toggle="modal" data-target="#query_modal">query</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
<div id="form_container" class="col-left-fixed">
|
||||
<div class="row center-block">
|
||||
<div class="btn-group query-and-save">
|
||||
<button type="button" class="btn btn-primary query">
|
||||
<i class="fa fa-bolt"></i>Query
|
||||
</button>
|
||||
{% if viz.form_data.slice_id %}
|
||||
<button type="button" class="btn btn-default" id="btn_overwrite">
|
||||
<i class="fa fa-save"></i>Overwrite
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-default" id="btn_save">
|
||||
<i class="fa fa-plus-circle"></i>Save as
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
{% for fieldset in form.fieldsets %}
|
||||
<div class="panel panel-default">
|
||||
{% if fieldset.label %}
|
||||
<div class="panel-heading">
|
||||
<span class="legend_label">{{ fieldset.label }}</span>
|
||||
{% if fieldset.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title="{{ fieldset.description }}"></i>
|
||||
{% endif %}
|
||||
<span class="collapser"> [-]</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel-body">
|
||||
{% for fieldname in fieldset.fields %}
|
||||
{% if not fieldname %}
|
||||
<hr/>
|
||||
{% elif fieldname is string %}
|
||||
{{ panofield(fieldname) }}
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
{% for name in fieldname %}
|
||||
<div class="col-xs-{{ (12 / fieldname|length) | int }}">
|
||||
{% if name %}
|
||||
{{ panofield(name) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<span class="legend_label">Filters</span>
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title="Filters are defined using comma delimited strings as in 'US,FR,Other'"></i>
|
||||
<span class="collapser"> [-]</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div id="flt0" style="display: none;">
|
||||
<span class="">{{ form.flt_col_0(class_="form-control inc") }}</span>
|
||||
<div class="row">
|
||||
<span class="col col-sm-4">{{ form.flt_op_0(class_="form-control inc") }}</span>
|
||||
<span class="col col-sm-6">{{ form.flt_eq_0(class_="form-control inc") }}</span>
|
||||
<button type="button" class="btn btn-default btn-sm remove" aria-label="Delete filter">
|
||||
<span class="fa fa-minus" aria-hidden="true"></span>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="filters"></div>
|
||||
<button type="button" id="plus" class="btn btn-default btn-sm" aria-label="Add a filter">
|
||||
<span class="fa fa-plus" aria-hidden="true"></span>
|
||||
<span>Add filter</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ form.slice_id() }}
|
||||
{{ form.slice_name() }}
|
||||
{{ form.collapsed_fieldsets() }}
|
||||
<input type="hidden" name="action" id="action" value="">
|
||||
<input type="hidden" name="datasource_name" value="{{ datasource.name }}">
|
||||
<input type="hidden" name="datasource_id" value="{{ datasource.id }}">
|
||||
<input type="hidden" name="datasource_type" value="{{ datasource.type }}">
|
||||
<input type="hidden" name="previous_viz_type" value="{{ viz.viz_type or "table" }}">
|
||||
</div>
|
||||
|
||||
<div class="col-offset">
|
||||
{% block messages %}{% endblock %}
|
||||
{% include 'appbuilder/flash.html' %}
|
||||
<div
|
||||
id="{{ viz.token }}"
|
||||
class="widget viz slice {{ viz.viz_type }}"
|
||||
data-slice="{{ viz.json_data }}"
|
||||
style="height: 700px;">
|
||||
<img src="{{ url_for("static", filename="assets/images/loading.gif") }}" class="loading" alt="loading">
|
||||
<div id="{{ viz.token }}_con" class="slice_container" style="height: 100%; width: 100%"></div>
|
||||
</div>
|
||||
<div class="credits pull-right">{{ "credits: " + viz.credits |safe if viz. credits else "" }}</div>
|
||||
</div>
|
||||
<div class="modal fade" id="query_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Query</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="query_container"></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="sourceinfo_modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">Datasource Description</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{{ datasource.description_markeddown | safe }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
<script src="/static/assets/javascripts/dist/explore.entry.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,6 +0,0 @@
|
||||
{% extends "caravel/basic.html" %}
|
||||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
<script src="/static/assets/javascripts/dist/index.entry.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,50 +0,0 @@
|
||||
{% extends "caravel/basic.html" %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
<div class="container-fluid">
|
||||
<div class="sqlcontent" style="display: none;">
|
||||
<h3>db: [{{ db }}]</h3>
|
||||
<div class="row interactions">
|
||||
<div class="col-xs-7">
|
||||
<input type="hidden" id="database_id" value="{{ db.id }}">
|
||||
<button class="btn btn-primary" id="run">Run!</button>
|
||||
<button class="btn btn-default" id="create_view">Create View</button>
|
||||
</div>
|
||||
<div class="col-xs-5">
|
||||
<select id="dbtable">
|
||||
{% for t in tables %}
|
||||
<option value="{{ t }}"
|
||||
{{ "selected" if t == table_name else '' }}>
|
||||
{{ t }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-default" id="select_star">SELECT *</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topsql row">
|
||||
<div class="col-xs-7 fillheight">
|
||||
<textarea id="sql" class="fillup"></textarea>
|
||||
</div>
|
||||
<div class="col-xs-5 fillheight">
|
||||
<div class="metadata fillup bordered"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="interactive">
|
||||
</div>
|
||||
<div id="results_section">
|
||||
<hr/>
|
||||
<img id="loading" width="25" style="display: none;" src="/static/assets/images/loading.gif">
|
||||
</div>
|
||||
<div>
|
||||
<div id="results" class="bordered" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block tail_js %}
|
||||
{{ super() }}
|
||||
<script src="/static/assets/javascripts/dist/sql.entry.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,21 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<script src="/static/assets/javascripts/dist/standalone.entry.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/caravel.css" />
|
||||
{% set CSS_THEME = appbuilder.get_app.config.get("CSS_THEME") %}
|
||||
{% set height = request.args.get("height", 700) %}
|
||||
{% if CSS_THEME %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ CSS_THEME }}" />
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="{{ viz.token }}"
|
||||
class="widget viz slice {{ viz.viz_type }}"
|
||||
data-slice="{{ viz.json_data }}"
|
||||
style="height: {{ height }}px;">
|
||||
<img src="{{ url_for("static", filename="img/loading.gif") }}" class="loading" alt="loading">
|
||||
<div id="{{ viz.token }}_con" class="slice_container" style="height: 100%; width: 100%"></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,65 +0,0 @@
|
||||
{% extends "caravel/basic.html" %}
|
||||
|
||||
{% block head_js %}
|
||||
{{ super() }}
|
||||
<script src="/static/assets/javascripts/dist/welcome.entry.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}Welcome!{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container welcome">
|
||||
<div class="header">
|
||||
<h3>Welcome!</h3>
|
||||
</div>
|
||||
<hr/>
|
||||
<div id="cal-heatmap"></div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<span class="pull-left">
|
||||
<h5><i class="fa fa-dashboard"></i> Dashboards</h5>
|
||||
</span>
|
||||
<span class="search pull-right"></span>
|
||||
<span class="pull-right">
|
||||
<h5><i class="fa fa-search"></i></h5>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<img class="loading" src="/static/assets/images/loading.gif"/>
|
||||
<table id="dash_table" class="table table-condensed" width="100%"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<span class="pull-left">
|
||||
<h5><i class="fa fa-bar-chart"></i> Slices</h5>
|
||||
</span>
|
||||
<span class="search pull-right"></span>
|
||||
<span class="pull-right">
|
||||
<h5><i class="fa fa-search"></i></h5>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<img class="loading" src="/static/assets/images/loading.gif"/>
|
||||
<table id="slice_table" class="table table-condensed" width="100%"></table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
{% endblock %}
|
||||
|
||||
226
caravel/utils.py
@@ -1,226 +0,0 @@
|
||||
"""Utility functions used across Caravel"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import parsedatetime
|
||||
from dateutil.parser import parse
|
||||
from flask import Markup
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
from markdown import markdown as md
|
||||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
|
||||
|
||||
class memoized(object): # noqa
|
||||
|
||||
"""Decorator that caches a function's return value each time it is called
|
||||
|
||||
If called later with the same arguments, the cached value is returned, and
|
||||
not re-evaluated.
|
||||
"""
|
||||
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.cache = {}
|
||||
|
||||
def __call__(self, *args):
|
||||
try:
|
||||
return self.cache[args]
|
||||
except KeyError:
|
||||
value = self.func(*args)
|
||||
self.cache[args] = value
|
||||
return value
|
||||
except TypeError:
|
||||
# uncachable -- for instance, passing a list as an argument.
|
||||
# Better to not cache than to blow up entirely.
|
||||
return self.func(*args)
|
||||
|
||||
def __repr__(self):
|
||||
"""Return the function's docstring."""
|
||||
return self.func.__doc__
|
||||
|
||||
def __get__(self, obj, objtype):
|
||||
"""Support instance methods."""
|
||||
return functools.partial(self.__call__, obj)
|
||||
|
||||
|
||||
def list_minus(l, minus):
|
||||
"""Returns l without what is in minus
|
||||
|
||||
>>> list_minus([1, 2, 3], [2])
|
||||
[1, 3]
|
||||
"""
|
||||
return [o for o in l if o not in minus]
|
||||
|
||||
|
||||
def parse_human_datetime(s):
|
||||
"""
|
||||
Returns ``datetime.datetime`` from human readable strings
|
||||
|
||||
>>> from datetime import date, timedelta
|
||||
>>> from dateutil.relativedelta import relativedelta
|
||||
>>> parse_human_datetime('2015-04-03')
|
||||
datetime.datetime(2015, 4, 3, 0, 0)
|
||||
>>> parse_human_datetime('2/3/1969')
|
||||
datetime.datetime(1969, 2, 3, 0, 0)
|
||||
>>> parse_human_datetime("now") <= datetime.now()
|
||||
True
|
||||
>>> parse_human_datetime("yesterday") <= datetime.now()
|
||||
True
|
||||
>>> date.today() - timedelta(1) == parse_human_datetime('yesterday').date()
|
||||
True
|
||||
>>> year_ago_1 = parse_human_datetime('one year ago').date()
|
||||
>>> year_ago_2 = (datetime.now() - relativedelta(years=1) ).date()
|
||||
>>> year_ago_1 == year_ago_2
|
||||
True
|
||||
"""
|
||||
try:
|
||||
dttm = parse(s)
|
||||
except Exception:
|
||||
try:
|
||||
cal = parsedatetime.Calendar()
|
||||
dttm = dttm_from_timtuple(cal.parse(s)[0])
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
raise ValueError("Couldn't parse date string [{}]".format(s))
|
||||
return dttm
|
||||
|
||||
|
||||
def dttm_from_timtuple(d):
|
||||
return datetime(
|
||||
d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
|
||||
|
||||
|
||||
def merge_perm(sm, permission_name, view_menu_name):
|
||||
pv = sm.find_permission_view_menu(permission_name, view_menu_name)
|
||||
if not pv:
|
||||
sm.add_permission_view_menu(permission_name, view_menu_name)
|
||||
|
||||
|
||||
def parse_human_timedelta(s):
|
||||
"""
|
||||
Returns ``datetime.datetime`` from natural language time deltas
|
||||
|
||||
>>> parse_human_datetime("now") <= datetime.now()
|
||||
True
|
||||
"""
|
||||
cal = parsedatetime.Calendar()
|
||||
dttm = dttm_from_timtuple(datetime.now().timetuple())
|
||||
d = cal.parse(s, dttm)[0]
|
||||
d = datetime(
|
||||
d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
|
||||
return d - dttm
|
||||
|
||||
|
||||
class JSONEncodedDict(TypeDecorator):
|
||||
|
||||
"""Represents an immutable structure as a json-encoded string."""
|
||||
|
||||
impl = TEXT
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
def init(caravel):
|
||||
"""Inits the Caravel application with security roles and such"""
|
||||
db = caravel.db
|
||||
models = caravel.models
|
||||
sm = caravel.appbuilder.sm
|
||||
alpha = sm.add_role("Alpha")
|
||||
admin = sm.add_role("Admin")
|
||||
|
||||
merge_perm(sm, 'all_datasource_access', 'all_datasource_access')
|
||||
|
||||
perms = db.session.query(ab_models.PermissionView).all()
|
||||
for perm in perms:
|
||||
if perm.permission.name == 'datasource_access':
|
||||
continue
|
||||
if perm.view_menu and perm.view_menu.name not in (
|
||||
'UserDBModelView', 'RoleModelView', 'ResetPasswordView',
|
||||
'Security'):
|
||||
sm.add_permission_role(alpha, perm)
|
||||
sm.add_permission_role(admin, perm)
|
||||
gamma = sm.add_role("Gamma")
|
||||
for perm in perms:
|
||||
if(
|
||||
perm.view_menu and perm.view_menu.name not in (
|
||||
'ResetPasswordView',
|
||||
'RoleModelView',
|
||||
'UserDBModelView',
|
||||
'Security') and
|
||||
perm.permission.name not in (
|
||||
'all_datasource_access',
|
||||
'can_add',
|
||||
'can_download',
|
||||
'can_delete',
|
||||
'can_edit',
|
||||
'can_save',
|
||||
'datasource_access',
|
||||
'muldelete',
|
||||
)):
|
||||
sm.add_permission_role(gamma, perm)
|
||||
session = db.session()
|
||||
table_perms = [
|
||||
table.perm for table in session.query(models.SqlaTable).all()]
|
||||
table_perms += [
|
||||
table.perm for table in session.query(models.DruidDatasource).all()]
|
||||
for table_perm in table_perms:
|
||||
merge_perm(sm, 'datasource_access', table_perm)
|
||||
|
||||
|
||||
def datetime_f(dttm):
|
||||
"""Formats datetime to take less room when it is recent"""
|
||||
if dttm:
|
||||
dttm = dttm.isoformat()
|
||||
now_iso = datetime.now().isoformat()
|
||||
if now_iso[:10] == dttm[:10]:
|
||||
dttm = dttm[11:]
|
||||
elif now_iso[:4] == dttm[:4]:
|
||||
dttm = dttm[5:]
|
||||
return "<nobr>{}</nobr>".format(dttm)
|
||||
|
||||
|
||||
def json_iso_dttm_ser(obj):
|
||||
"""
|
||||
json serializer that deals with dates
|
||||
|
||||
>>> dttm = datetime(1970, 1, 1)
|
||||
>>> json.dumps({'dttm': dttm}, default=json_iso_dttm_ser)
|
||||
'{"dttm": "1970-01-01T00:00:00"}'
|
||||
"""
|
||||
if isinstance(obj, datetime):
|
||||
obj = obj.isoformat()
|
||||
return obj
|
||||
|
||||
|
||||
def markdown(s, markup_wrap=False):
|
||||
s = s or ''
|
||||
s = md(s, [
|
||||
'markdown.extensions.tables',
|
||||
'markdown.extensions.fenced_code',
|
||||
'markdown.extensions.codehilite',
|
||||
])
|
||||
if markup_wrap:
|
||||
s = Markup(s)
|
||||
return s
|
||||
|
||||
|
||||
def readfile(filepath):
|
||||
with open(filepath) as f:
|
||||
content = f.read()
|
||||
return content
|
||||
909
caravel/views.py
@@ -1,909 +0,0 @@
|
||||
"""Flask web views for Caravel"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
||||
import pandas as pd
|
||||
import sqlalchemy as sqla
|
||||
from flask import (
|
||||
g, request, redirect, flash, Response, render_template, Markup)
|
||||
from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose
|
||||
from flask.ext.appbuilder.actions import action
|
||||
from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
|
||||
from flask.ext.appbuilder.security.decorators import has_access
|
||||
from pydruid.client import doublesum
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.sql.expression import TextAsFrom
|
||||
from werkzeug.routing import BaseConverter
|
||||
from wtforms.validators import ValidationError
|
||||
|
||||
from caravel import appbuilder, db, models, viz, utils, app, sm, ascii_art
|
||||
|
||||
config = app.config
|
||||
log_this = models.Log.log_this
|
||||
|
||||
|
||||
def validate_json(form, field): # noqa
|
||||
try:
|
||||
json.loads(field.data)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
raise ValidationError("json isn't valid")
|
||||
|
||||
|
||||
def generate_download_headers(extension):
|
||||
filename = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
content_disp = "attachment; filename={}.{}".format(filename, extension)
|
||||
headers = {
|
||||
"Content-Disposition": content_disp,
|
||||
}
|
||||
return headers
|
||||
|
||||
|
||||
class DeleteMixin(object):
|
||||
@action(
|
||||
"muldelete", "Delete", "Delete all Really?", "fa-trash", single=False)
|
||||
def muldelete(self, items):
|
||||
self.datamodel.delete_all(items)
|
||||
self.update_redirect()
|
||||
return redirect(self.get_redirect())
|
||||
|
||||
|
||||
class CaravelModelView(ModelView):
|
||||
page_size = 500
|
||||
|
||||
|
||||
class TableColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
||||
datamodel = SQLAInterface(models.TableColumn)
|
||||
can_delete = False
|
||||
edit_columns = [
|
||||
'column_name', 'description', 'groupby', 'filterable', 'table',
|
||||
'count_distinct', 'sum', 'min', 'max', 'expression', 'is_dttm']
|
||||
add_columns = edit_columns
|
||||
list_columns = [
|
||||
'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
|
||||
'sum', 'min', 'max', 'is_dttm']
|
||||
page_size = 500
|
||||
description_columns = {
|
||||
'is_dttm': (
|
||||
"Whether to make this column available as a "
|
||||
"[Time Granularity] option, column has to be DATETIME or "
|
||||
"DATETIME-like"),
|
||||
'expression': utils.markdown(
|
||||
"a valid SQL expression as supported by the underlying backend. "
|
||||
"Example: `substr(name, 1, 1)`", True),
|
||||
}
|
||||
appbuilder.add_view_no_menu(TableColumnInlineView)
|
||||
|
||||
|
||||
|
||||
class DruidColumnInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
||||
datamodel = SQLAInterface(models.DruidColumn)
|
||||
edit_columns = [
|
||||
'column_name', 'description', 'datasource', 'groupby',
|
||||
'count_distinct', 'sum', 'min', 'max']
|
||||
list_columns = [
|
||||
'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
|
||||
'sum', 'min', 'max']
|
||||
can_delete = False
|
||||
page_size = 500
|
||||
|
||||
def post_update(self, col):
|
||||
col.generate_metrics()
|
||||
|
||||
appbuilder.add_view_no_menu(DruidColumnInlineView)
|
||||
|
||||
|
||||
class SqlMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
||||
datamodel = SQLAInterface(models.SqlMetric)
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
||||
edit_columns = [
|
||||
'metric_name', 'description', 'verbose_name', 'metric_type',
|
||||
'expression', 'table']
|
||||
description_columns = {
|
||||
'expression': utils.markdown(
|
||||
"a valid SQL expression as supported by the underlying backend. "
|
||||
"Example: `count(DISTINCT userid)`", True),
|
||||
}
|
||||
add_columns = edit_columns
|
||||
page_size = 500
|
||||
appbuilder.add_view_no_menu(SqlMetricInlineView)
|
||||
|
||||
|
||||
class DruidMetricInlineView(CompactCRUDMixin, CaravelModelView): # noqa
|
||||
datamodel = SQLAInterface(models.DruidMetric)
|
||||
list_columns = ['metric_name', 'verbose_name', 'metric_type']
|
||||
edit_columns = [
|
||||
'metric_name', 'description', 'verbose_name', 'metric_type',
|
||||
'datasource', 'json']
|
||||
add_columns = [
|
||||
'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json']
|
||||
page_size = 500
|
||||
validators_columns = {
|
||||
'json': [validate_json],
|
||||
}
|
||||
appbuilder.add_view_no_menu(DruidMetricInlineView)
|
||||
|
||||
|
||||
class DatabaseView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.Database)
|
||||
list_columns = ['database_name', 'sql_link', 'created_by_', 'changed_on']
|
||||
order_columns = utils.list_minus(list_columns, ['created_by_'])
|
||||
add_columns = [
|
||||
'database_name', 'sqlalchemy_uri', 'cache_timeout', 'extra']
|
||||
show_columns = add_columns
|
||||
search_exclude_columns = ('password',)
|
||||
edit_columns = add_columns
|
||||
add_template = "caravel/models/database/add.html"
|
||||
edit_template = "caravel/models/database/edit.html"
|
||||
base_order = ('changed_on', 'desc')
|
||||
description_columns = {
|
||||
'sqlalchemy_uri': (
|
||||
"Refer to the SqlAlchemy docs for more information on how "
|
||||
"to structure your URI here: "
|
||||
"http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html"),
|
||||
'extra': utils.markdown(
|
||||
"JSON string containing extra configuration elements. "
|
||||
"The ``engine_params`` object gets unpacked into the "
|
||||
"[sqlalchemy.create_engine]"
|
||||
"(http://docs.sqlalchemy.org/en/latest/core/engines.html#"
|
||||
"sqlalchemy.create_engine) call, while the ``metadata_params`` "
|
||||
"gets unpacked into the [sqlalchemy.MetaData]"
|
||||
"(http://docs.sqlalchemy.org/en/rel_1_0/core/metadata.html"
|
||||
"#sqlalchemy.schema.MetaData) call. ", True),
|
||||
}
|
||||
|
||||
def pre_add(self, db):
|
||||
conn = sqla.engine.url.make_url(db.sqlalchemy_uri)
|
||||
db.password = conn.password
|
||||
conn.password = "X" * 10 if conn.password else None
|
||||
db.sqlalchemy_uri = str(conn) # hides the password
|
||||
|
||||
def pre_update(self, db):
|
||||
self.pre_add(db)
|
||||
|
||||
|
||||
appbuilder.add_view(
|
||||
DatabaseView,
|
||||
"Databases",
|
||||
icon="fa-database",
|
||||
category="Sources",
|
||||
category_icon='fa-database',)
|
||||
|
||||
|
||||
class TableModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.SqlaTable)
|
||||
list_columns = [
|
||||
'table_link', 'database', 'sql_link', 'is_featured',
|
||||
'changed_by_', 'changed_on']
|
||||
add_columns = [
|
||||
'table_name', 'database', 'schema',
|
||||
'default_endpoint', 'offset', 'cache_timeout']
|
||||
edit_columns = [
|
||||
'table_name', 'is_featured', 'database', 'schema', 'description', 'owner',
|
||||
'main_dttm_col', 'default_endpoint', 'offset', 'cache_timeout']
|
||||
related_views = [TableColumnInlineView, SqlMetricInlineView]
|
||||
base_order = ('changed_on', 'desc')
|
||||
description_columns = {
|
||||
'offset': "Timezone offset (in hours) for this datasource",
|
||||
'schema': (
|
||||
"Schema, as used only in some databases like Postgres, Redshift "
|
||||
"and DB2"),
|
||||
'description': Markup(
|
||||
"Supports <a href='https://daringfireball.net/projects/markdown/'>"
|
||||
"markdown</a>"),
|
||||
}
|
||||
|
||||
def post_add(self, table):
|
||||
try:
|
||||
table.fetch_metadata()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
flash(
|
||||
"Table [{}] doesn't seem to exist, "
|
||||
"couldn't fetch metadata".format(table.table_name),
|
||||
"danger")
|
||||
utils.merge_perm(sm, 'datasource_access', table.perm)
|
||||
|
||||
def post_update(self, table):
|
||||
self.post_add(table)
|
||||
|
||||
appbuilder.add_view(
|
||||
TableModelView,
|
||||
"Tables",
|
||||
category="Sources",
|
||||
icon='fa-table',)
|
||||
|
||||
|
||||
appbuilder.add_separator("Sources")
|
||||
|
||||
|
||||
class DruidClusterModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.DruidCluster)
|
||||
add_columns = [
|
||||
'cluster_name',
|
||||
'coordinator_host', 'coordinator_port', 'coordinator_endpoint',
|
||||
'broker_host', 'broker_port', 'broker_endpoint',
|
||||
]
|
||||
edit_columns = add_columns
|
||||
list_columns = ['cluster_name', 'metadata_last_refreshed']
|
||||
|
||||
|
||||
if config['DRUID_IS_ACTIVE']:
|
||||
appbuilder.add_view(
|
||||
DruidClusterModelView,
|
||||
"Druid Clusters",
|
||||
icon="fa-cubes",
|
||||
category="Sources",
|
||||
category_icon='fa-database',)
|
||||
|
||||
|
||||
class SliceModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.Slice)
|
||||
add_template = "caravel/add_slice.html"
|
||||
can_add = False
|
||||
label_columns = {
|
||||
'created_by_': 'Creator',
|
||||
'datasource_link': 'Datasource',
|
||||
}
|
||||
list_columns = [
|
||||
'slice_link', 'viz_type',
|
||||
'datasource_link', 'created_by_', 'modified']
|
||||
order_columns = utils.list_minus(list_columns, ['created_by_', 'modified'])
|
||||
edit_columns = [
|
||||
'slice_name', 'description', 'viz_type', 'druid_datasource',
|
||||
'table', 'dashboards', 'params', 'cache_timeout']
|
||||
base_order = ('changed_on', 'desc')
|
||||
description_columns = {
|
||||
'description': Markup(
|
||||
"The content here can be displayed as widget headers in the "
|
||||
"dashboard view. Supports "
|
||||
"<a href='https://daringfireball.net/projects/markdown/'>"
|
||||
"markdown</a>"),
|
||||
}
|
||||
|
||||
appbuilder.add_view(
|
||||
SliceModelView,
|
||||
"Slices",
|
||||
icon="fa-bar-chart",
|
||||
category="",
|
||||
category_icon='',)
|
||||
|
||||
|
||||
class SliceAsync(SliceModelView): # noqa
|
||||
list_columns = [
|
||||
'slice_link', 'viz_type',
|
||||
'created_by_', 'modified', 'icons']
|
||||
label_columns = {
|
||||
'icons': ' ',
|
||||
'created_by_': 'Creator',
|
||||
'viz_type': 'Type',
|
||||
'slice_link': 'Slice',
|
||||
}
|
||||
|
||||
appbuilder.add_view_no_menu(SliceAsync)
|
||||
|
||||
|
||||
class DashboardModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
label_columns = {
|
||||
'created_by_': 'Creator',
|
||||
}
|
||||
list_columns = ['dashboard_link', 'created_by_', 'modified']
|
||||
order_columns = utils.list_minus(list_columns, ['created_by_', 'modified'])
|
||||
edit_columns = [
|
||||
'dashboard_title', 'slug', 'slices', 'position_json', 'css',
|
||||
'json_metadata']
|
||||
add_columns = edit_columns
|
||||
base_order = ('changed_on', 'desc')
|
||||
description_columns = {
|
||||
'position_json': (
|
||||
"This json object describes the positioning of the widgets in "
|
||||
"the dashboard. It is dynamically generated when adjusting "
|
||||
"the widgets size and positions by using drag & drop in "
|
||||
"the dashboard view"),
|
||||
'css': (
|
||||
"The css for individual dashboards can be altered here, or "
|
||||
"in the dashboard view where changes are immediately "
|
||||
"visible"),
|
||||
'slug': "To get a readable URL for your dashboard",
|
||||
}
|
||||
|
||||
def pre_add(self, obj):
|
||||
obj.slug = obj.slug.strip() or None
|
||||
if obj.slug:
|
||||
obj.slug = obj.slug.replace(" ", "-")
|
||||
obj.slug = re.sub(r'\W+', '', obj.slug)
|
||||
|
||||
def pre_update(self, obj):
|
||||
self.pre_add(obj)
|
||||
|
||||
|
||||
appbuilder.add_view(
|
||||
DashboardModelView,
|
||||
"Dashboards",
|
||||
icon="fa-dashboard",
|
||||
category="",
|
||||
category_icon='',)
|
||||
|
||||
|
||||
class DashboardModelViewAsync(DashboardModelView): # noqa
|
||||
list_columns = ['dashboard_link', 'created_by_', 'modified']
|
||||
label_columns = {
|
||||
'created_by_': 'Creator',
|
||||
'dashboard_link': 'Dashboard',
|
||||
}
|
||||
|
||||
appbuilder.add_view_no_menu(DashboardModelViewAsync)
|
||||
|
||||
|
||||
class LogModelView(CaravelModelView):
|
||||
datamodel = SQLAInterface(models.Log)
|
||||
list_columns = ('user', 'action', 'dttm')
|
||||
edit_columns = ('user', 'action', 'dttm', 'json')
|
||||
base_order = ('dttm', 'desc')
|
||||
|
||||
appbuilder.add_view(
|
||||
LogModelView,
|
||||
"Action Log",
|
||||
category="Security",
|
||||
icon="fa-list-ol")
|
||||
|
||||
|
||||
class DruidDatasourceModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.DruidDatasource)
|
||||
list_columns = [
|
||||
'datasource_link', 'cluster', 'owner',
|
||||
'created_by_', 'created_on',
|
||||
'changed_by_', 'changed_on',
|
||||
'offset']
|
||||
order_columns = utils.list_minus(
|
||||
list_columns, ['created_by_', 'changed_by_'])
|
||||
related_views = [DruidColumnInlineView, DruidMetricInlineView]
|
||||
edit_columns = [
|
||||
'datasource_name', 'cluster', 'description', 'owner',
|
||||
'is_featured', 'is_hidden', 'default_endpoint', 'offset',
|
||||
'cache_timeout']
|
||||
page_size = 500
|
||||
base_order = ('datasource_name', 'asc')
|
||||
description_columns = {
|
||||
'offset': "Timezone offset (in hours) for this datasource",
|
||||
'description': Markup(
|
||||
"Supports <a href='"
|
||||
"https://daringfireball.net/projects/markdown/'>markdown</a>"),
|
||||
}
|
||||
|
||||
def post_add(self, datasource):
|
||||
datasource.generate_metrics()
|
||||
utils.merge_perm(sm, 'datasource_access', datasource.perm)
|
||||
|
||||
def post_update(self, datasource):
|
||||
self.post_add(datasource)
|
||||
|
||||
if config['DRUID_IS_ACTIVE']:
|
||||
appbuilder.add_view(
|
||||
DruidDatasourceModelView,
|
||||
"Druid Datasources",
|
||||
category="Sources",
|
||||
icon="fa-cube")
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
def health():
|
||||
return "OK"
|
||||
|
||||
|
||||
@app.route('/ping')
|
||||
def ping():
|
||||
return "OK"
|
||||
|
||||
|
||||
class R(BaseView):
|
||||
|
||||
"""used for short urls"""
|
||||
|
||||
@log_this
|
||||
@expose("/<url_id>")
|
||||
def index(self, url_id):
|
||||
url = db.session.query(models.Url).filter_by(id=url_id).first()
|
||||
if url:
|
||||
print(url.url)
|
||||
return redirect('/' + url.url)
|
||||
else:
|
||||
flash("URL to nowhere...", "danger")
|
||||
return redirect('/')
|
||||
|
||||
@log_this
|
||||
@expose("/shortner/", methods=['POST', 'GET'])
|
||||
def shortner(self):
|
||||
url = request.form.get('data')
|
||||
obj = models.Url(url=url)
|
||||
db.session.add(obj)
|
||||
db.session.commit()
|
||||
return("{request.headers[Host]}/r/{obj.id}".format(
|
||||
request=request, obj=obj))
|
||||
|
||||
@expose("/msg/")
|
||||
def msg(self):
|
||||
"""Redirects to specified url while flash a message"""
|
||||
flash(request.args.get("msg"), "info")
|
||||
return redirect(request.args.get("url"))
|
||||
|
||||
appbuilder.add_view_no_menu(R)
|
||||
|
||||
|
||||
class Caravel(BaseView):
|
||||
|
||||
"""The base views for Caravel!"""
|
||||
|
||||
@has_access
|
||||
@expose("/explore/<datasource_type>/<datasource_id>/")
|
||||
@expose("/datasource/<datasource_type>/<datasource_id>/") # Legacy url
|
||||
@log_this
|
||||
def explore(self, datasource_type, datasource_id):
|
||||
error_redirect = '/slicemodelview/list/'
|
||||
datasource_class = models.SqlaTable \
|
||||
if datasource_type == "table" else models.DruidDatasource
|
||||
datasources = (
|
||||
db.session
|
||||
.query(datasource_class)
|
||||
.all()
|
||||
)
|
||||
datasources = sorted(datasources, key=lambda ds: ds.full_name)
|
||||
datasource = [ds for ds in datasources if int(datasource_id) == ds.id]
|
||||
datasource = datasource[0] if datasource else None
|
||||
slice_id = request.args.get("slice_id")
|
||||
slc = None
|
||||
if slice_id:
|
||||
slc = (
|
||||
db.session.query(models.Slice)
|
||||
.filter_by(id=slice_id)
|
||||
.first()
|
||||
)
|
||||
if not datasource:
|
||||
flash("The datasource seems to have been deleted", "alert")
|
||||
return redirect(error_redirect)
|
||||
|
||||
all_datasource_access = self.appbuilder.sm.has_access(
|
||||
'all_datasource_access', 'all_datasource_access')
|
||||
datasource_access = self.appbuilder.sm.has_access(
|
||||
'datasource_access', datasource.perm)
|
||||
if not (all_datasource_access or datasource_access):
|
||||
flash("You don't seem to have access to this datasource", "danger")
|
||||
return redirect(error_redirect)
|
||||
|
||||
action = request.args.get('action')
|
||||
if action in ('save', 'overwrite'):
|
||||
return self.save(request.args, slc)
|
||||
|
||||
viz_type = request.args.get("viz_type")
|
||||
if not viz_type and datasource.default_endpoint:
|
||||
return redirect(datasource.default_endpoint)
|
||||
if not viz_type:
|
||||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
datasource,
|
||||
form_data=request.args,
|
||||
slice_=slc)
|
||||
if request.args.get("json") == "true":
|
||||
status = 200
|
||||
try:
|
||||
payload = obj.get_json()
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
if config.get("DEBUG"):
|
||||
raise e
|
||||
payload = str(e)
|
||||
status = 500
|
||||
resp = Response(
|
||||
payload,
|
||||
status=status,
|
||||
headers=generate_download_headers("json"),
|
||||
mimetype="application/json")
|
||||
return resp
|
||||
elif request.args.get("csv") == "true":
|
||||
status = 200
|
||||
payload = obj.get_csv()
|
||||
return Response(
|
||||
payload,
|
||||
status=status,
|
||||
headers=generate_download_headers("csv"),
|
||||
mimetype="application/csv")
|
||||
else:
|
||||
if request.args.get("standalone") == "true":
|
||||
template = "caravel/standalone.html"
|
||||
else:
|
||||
template = "caravel/explore.html"
|
||||
|
||||
resp = self.render_template(
|
||||
template, viz=obj, slice=slc, datasources=datasources)
|
||||
try:
|
||||
pass
|
||||
except Exception as e:
|
||||
if config.get("DEBUG"):
|
||||
raise(e)
|
||||
return Response(
|
||||
str(e),
|
||||
status=500,
|
||||
mimetype="application/json")
|
||||
return resp
|
||||
|
||||
def save(self, args, slc):
|
||||
"""Saves (inserts or overwrite a slice) """
|
||||
session = db.session()
|
||||
slice_name = args.get('slice_name')
|
||||
action = args.get('action')
|
||||
|
||||
# TODO use form processing form wtforms
|
||||
d = args.to_dict(flat=False)
|
||||
del d['action']
|
||||
del d['previous_viz_type']
|
||||
as_list = ('metrics', 'groupby', 'columns')
|
||||
for k in d:
|
||||
v = d.get(k)
|
||||
if k in as_list and not isinstance(v, list):
|
||||
d[k] = [v] if v else []
|
||||
if k not in as_list and isinstance(v, list):
|
||||
d[k] = v[0]
|
||||
|
||||
table_id = druid_datasource_id = None
|
||||
datasource_type = args.get('datasource_type')
|
||||
if datasource_type in ('datasource', 'druid'):
|
||||
druid_datasource_id = args.get('datasource_id')
|
||||
elif datasource_type == 'table':
|
||||
table_id = args.get('datasource_id')
|
||||
|
||||
if action == "save":
|
||||
slc = models.Slice()
|
||||
msg = "Slice [{}] has been saved".format(slice_name)
|
||||
elif action == "overwrite":
|
||||
msg = "Slice [{}] has been overwritten".format(slice_name)
|
||||
|
||||
slc.params = json.dumps(d, indent=4, sort_keys=True)
|
||||
slc.datasource_name = args.get('datasource_name')
|
||||
slc.viz_type = args.get('viz_type')
|
||||
slc.druid_datasource_id = druid_datasource_id
|
||||
slc.table_id = table_id
|
||||
slc.datasource_type = datasource_type
|
||||
slc.slice_name = slice_name
|
||||
|
||||
if action == "save":
|
||||
session.add(slc)
|
||||
elif action == "overwrite":
|
||||
session.merge(slc)
|
||||
session.commit()
|
||||
flash(msg, "info")
|
||||
return redirect(slc.slice_url)
|
||||
|
||||
@has_access
|
||||
@expose("/checkbox/<model_view>/<id_>/<attr>/<value>", methods=['GET'])
|
||||
def checkbox(self, model_view, id_, attr, value):
|
||||
"""endpoint for checking/unchecking any boolean in a sqla model"""
|
||||
model = None
|
||||
if model_view == 'TableColumnInlineView':
|
||||
model = models.TableColumn
|
||||
elif model_view == 'DruidColumnInlineView':
|
||||
model = models.DruidColumn
|
||||
|
||||
obj = db.session.query(model).filter_by(id=id_).first()
|
||||
if obj:
|
||||
setattr(obj, attr, value == 'true')
|
||||
db.session.commit()
|
||||
return Response("OK", mimetype="application/json")
|
||||
|
||||
@has_access
|
||||
@expose("/activity_per_day")
|
||||
def activity_per_day(self):
|
||||
"""endpoint to power the calendar heatmap on the welcome page"""
|
||||
Log = models.Log # noqa
|
||||
qry = (
|
||||
db.session
|
||||
.query(
|
||||
Log.dt,
|
||||
sqla.func.count())
|
||||
.group_by(Log.dt)
|
||||
.all()
|
||||
)
|
||||
payload = {str(time.mktime(dt.timetuple())): ccount for dt, ccount in qry if dt}
|
||||
return Response(json.dumps(payload), mimetype="application/json")
|
||||
|
||||
@has_access
|
||||
@expose("/save_dash/<dashboard_id>/", methods=['GET', 'POST'])
|
||||
def save_dash(self, dashboard_id):
|
||||
"""Save a dashboard's metadata"""
|
||||
data = json.loads(request.form.get('data'))
|
||||
positions = data['positions']
|
||||
slice_ids = [int(d['slice_id']) for d in positions]
|
||||
session = db.session()
|
||||
Dash = models.Dashboard # noqa
|
||||
dash = session.query(Dash).filter_by(id=dashboard_id).first()
|
||||
dash.slices = [o for o in dash.slices if o.id in slice_ids]
|
||||
dash.position_json = json.dumps(data['positions'], indent=4)
|
||||
md = dash.metadata_dejson
|
||||
if 'filter_immune_slices' not in md:
|
||||
md['filter_immune_slices'] = []
|
||||
md['expanded_slices'] = data['expanded_slices']
|
||||
dash.json_metadata = json.dumps(md, indent=4)
|
||||
dash.css = data['css']
|
||||
session.merge(dash)
|
||||
session.commit()
|
||||
session.close()
|
||||
return "SUCCESS"
|
||||
|
||||
@has_access
|
||||
@expose("/testconn", methods=["POST", "GET"])
|
||||
def testconn(self):
|
||||
"""Tests a sqla connection"""
|
||||
try:
|
||||
uri = request.json.get('uri')
|
||||
connect_args = request.json.get('extras', {}).get('engine_params', {}).get('connect_args', {})
|
||||
engine = create_engine(uri, connect_args=connect_args)
|
||||
engine.connect()
|
||||
return json.dumps(engine.table_names(), indent=4)
|
||||
except Exception:
|
||||
return Response(
|
||||
traceback.format_exc(),
|
||||
status=500,
|
||||
mimetype="application/json")
|
||||
|
||||
@expose("/favstar/<class_name>/<obj_id>/<action>/")
|
||||
def favstar(self, class_name, obj_id, action):
|
||||
session = db.session()
|
||||
FavStar = models.FavStar # noqa
|
||||
count = 0
|
||||
favs = session.query(FavStar).filter_by(
|
||||
class_name=class_name, obj_id=obj_id, user_id=g.user.id).all()
|
||||
if action == 'select':
|
||||
if not favs:
|
||||
session.add(
|
||||
FavStar(
|
||||
class_name=class_name, obj_id=obj_id, user_id=g.user.id,
|
||||
dttm=datetime.now()))
|
||||
count = 1
|
||||
elif action == 'unselect':
|
||||
for fav in favs:
|
||||
session.delete(fav)
|
||||
else:
|
||||
count = len(favs)
|
||||
session.commit()
|
||||
return Response(
|
||||
json.dumps({'count': count}),
|
||||
mimetype="application/json")
|
||||
|
||||
@has_access
|
||||
@expose("/dashboard/<dashboard_id>/")
|
||||
def dashboard(self, dashboard_id):
|
||||
"""Server side rendering for a dashboard"""
|
||||
session = db.session()
|
||||
qry = session.query(models.Dashboard)
|
||||
if dashboard_id.isdigit():
|
||||
qry = qry.filter_by(id=int(dashboard_id))
|
||||
else:
|
||||
qry = qry.filter_by(slug=dashboard_id)
|
||||
|
||||
templates = session.query(models.CssTemplate).all()
|
||||
dash = qry.first()
|
||||
|
||||
# Hack to log the dashboard_id properly, even when getting a slug
|
||||
@log_this
|
||||
def dashboard(**kwargs): # noqa
|
||||
pass
|
||||
dashboard(dashboard_id=dash.id)
|
||||
|
||||
pos_dict = {}
|
||||
if dash.position_json:
|
||||
pos_dict = {
|
||||
int(o['slice_id']): o
|
||||
for o in json.loads(dash.position_json)}
|
||||
return self.render_template(
|
||||
"caravel/dashboard.html", dashboard=dash,
|
||||
templates=templates,
|
||||
pos_dict=pos_dict)
|
||||
|
||||
@has_access
|
||||
@expose("/sql/<database_id>/")
|
||||
@log_this
|
||||
def sql(self, database_id):
|
||||
mydb = db.session.query(
|
||||
models.Database).filter_by(id=database_id).first()
|
||||
engine = mydb.get_sqla_engine()
|
||||
tables = engine.table_names()
|
||||
|
||||
table_name = request.args.get('table_name')
|
||||
return self.render_template(
|
||||
"caravel/sql.html",
|
||||
tables=tables,
|
||||
table_name=table_name,
|
||||
db=mydb)
|
||||
|
||||
@has_access
|
||||
@expose("/table/<database_id>/<table_name>/")
|
||||
@log_this
|
||||
def table(self, database_id, table_name):
|
||||
mydb = db.session.query(
|
||||
models.Database).filter_by(id=database_id).first()
|
||||
cols = mydb.get_columns(table_name)
|
||||
df = pd.DataFrame([(c['name'], c['type']) for c in cols])
|
||||
df.columns = ['col', 'type']
|
||||
tbl_cls = (
|
||||
"dataframe table table-striped table-bordered "
|
||||
"table-condensed sql_results").split(' ')
|
||||
return self.render_template(
|
||||
"caravel/ajah.html",
|
||||
content=df.to_html(
|
||||
index=False,
|
||||
na_rep='',
|
||||
classes=tbl_cls))
|
||||
|
||||
@has_access
|
||||
@expose("/select_star/<database_id>/<table_name>/")
|
||||
@log_this
|
||||
def select_star(self, database_id, table_name):
|
||||
mydb = db.session.query(
|
||||
models.Database).filter_by(id=database_id).first()
|
||||
t = mydb.get_table(table_name)
|
||||
fields = ", ".join(
|
||||
[c.name for c in t.columns] or "*")
|
||||
s = "SELECT\n{}\nFROM {}".format(fields, table_name)
|
||||
return self.render_template(
|
||||
"caravel/ajah.html",
|
||||
content=s
|
||||
)
|
||||
|
||||
@has_access
|
||||
@expose("/runsql/", methods=['POST', 'GET'])
|
||||
@log_this
|
||||
def runsql(self):
|
||||
"""Runs arbitrary sql and returns and html table"""
|
||||
session = db.session()
|
||||
limit = 1000
|
||||
data = json.loads(request.form.get('data'))
|
||||
sql = data.get('sql')
|
||||
database_id = data.get('database_id')
|
||||
mydb = session.query(models.Database).filter_by(id=database_id).first()
|
||||
|
||||
if (
|
||||
not self.appbuilder.sm.has_access(
|
||||
'all_datasource_access', 'all_datasource_access')):
|
||||
raise Exception("test")
|
||||
content = ""
|
||||
if mydb:
|
||||
eng = mydb.get_sqla_engine()
|
||||
if limit:
|
||||
sql = sql.strip().strip(';')
|
||||
qry = (
|
||||
select('*')
|
||||
.select_from(TextAsFrom(text(sql), ['*']).alias('inner_qry'))
|
||||
.limit(limit)
|
||||
)
|
||||
sql = str(qry.compile(eng, compile_kwargs={"literal_binds": True}))
|
||||
try:
|
||||
df = pd.read_sql_query(sql=sql, con=eng)
|
||||
content = df.to_html(
|
||||
index=False,
|
||||
na_rep='',
|
||||
classes=(
|
||||
"dataframe table table-striped table-bordered "
|
||||
"table-condensed sql_results").split(' '))
|
||||
except Exception as e:
|
||||
content = (
|
||||
'<div class="alert alert-danger">'
|
||||
"{}</div>"
|
||||
).format(e.message)
|
||||
session.commit()
|
||||
return content
|
||||
|
||||
@has_access
|
||||
@expose("/refresh_datasources/")
|
||||
def refresh_datasources(self):
|
||||
"""endpoint that refreshes druid datasources metadata"""
|
||||
session = db.session()
|
||||
for cluster in session.query(models.DruidCluster).all():
|
||||
try:
|
||||
cluster.refresh_datasources()
|
||||
except Exception as e:
|
||||
flash(
|
||||
"Error while processing cluster '{}'\n{}".format(
|
||||
cluster, str(e)),
|
||||
"danger")
|
||||
logging.exception(e)
|
||||
return redirect('/druidclustermodelview/list/')
|
||||
cluster.metadata_last_refreshed = datetime.now()
|
||||
flash(
|
||||
"Refreshed metadata from cluster "
|
||||
"[" + cluster.cluster_name + "]",
|
||||
'info')
|
||||
session.commit()
|
||||
return redirect("/druiddatasourcemodelview/list/")
|
||||
|
||||
@expose("/autocomplete/<datasource>/<column>/")
|
||||
def autocomplete(self, datasource, column):
|
||||
"""used for filter autocomplete"""
|
||||
client = utils.get_pydruid_client()
|
||||
top = client.topn(
|
||||
datasource=datasource,
|
||||
granularity='all',
|
||||
intervals='2013-10-04/2020-10-10',
|
||||
aggregations={"count": doublesum("count")},
|
||||
dimension=column,
|
||||
metric='count',
|
||||
threshold=1000,
|
||||
)
|
||||
values = sorted([d[column] for d in top[0]['result']])
|
||||
return json.dumps(values)
|
||||
|
||||
@app.errorhandler(500)
|
||||
def show_traceback(self):
|
||||
if config.get("SHOW_STACKTRACE"):
|
||||
error_msg = traceback.format_exc()
|
||||
else:
|
||||
error_msg = "FATAL ERROR\n"
|
||||
error_msg = (
|
||||
"Stacktrace is hidden. Change the SHOW_STACKTRACE "
|
||||
"configuration setting to enable it")
|
||||
return render_template(
|
||||
'caravel/traceback.html',
|
||||
error_msg=error_msg,
|
||||
title=ascii_art.stacktrace,
|
||||
art=ascii_art.error), 500
|
||||
|
||||
@has_access
|
||||
@expose("/welcome")
|
||||
def welcome(self):
|
||||
"""Personalized welcome page"""
|
||||
return self.render_template('caravel/welcome.html', utils=utils)
|
||||
|
||||
|
||||
appbuilder.add_view_no_menu(Caravel)
|
||||
|
||||
if config['DRUID_IS_ACTIVE']:
|
||||
appbuilder.add_link(
|
||||
"Refresh Druid Metadata",
|
||||
href='/caravel/refresh_datasources/',
|
||||
category='Sources',
|
||||
category_icon='fa-database',
|
||||
icon="fa-cog")
|
||||
|
||||
|
||||
class CssTemplateModelView(CaravelModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.CssTemplate)
|
||||
list_columns = ['template_name']
|
||||
edit_columns = ['template_name', 'css']
|
||||
add_columns = edit_columns
|
||||
|
||||
appbuilder.add_separator("Sources")
|
||||
appbuilder.add_view(
|
||||
CssTemplateModelView,
|
||||
"CSS Templates",
|
||||
icon="fa-css3",
|
||||
category="Sources",
|
||||
category_icon='')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------
|
||||
# Redirecting URL from previous names
|
||||
class RegexConverter(BaseConverter):
|
||||
def __init__(self, url_map, *items):
|
||||
super(RegexConverter, self).__init__(url_map)
|
||||
self.regex = items[0]
|
||||
app.url_map.converters['regex'] = RegexConverter
|
||||
|
||||
|
||||
@app.route('/<regex("panoramix\/.*"):url>')
|
||||
def panoramix(url): # noqa
|
||||
return redirect(request.full_path.replace('panoramix', 'caravel'))
|
||||
|
||||
|
||||
@app.route('/<regex("dashed\/.*"):url>')
|
||||
def dashed(url): # noqa
|
||||
return redirect(request.full_path.replace('dashed', 'caravel'))
|
||||
# ---------------------------------------------------------------------
|
||||
@@ -1,5 +1,11 @@
|
||||
codeclimate-test-reporter
|
||||
coveralls
|
||||
flake8
|
||||
mock
|
||||
mysqlclient
|
||||
nose
|
||||
psycopg2
|
||||
pyyaml
|
||||
sphinx
|
||||
sphinx_bootstrap_theme
|
||||
sphinx-rtd-theme
|
||||
sphinxcontrib.youtube
|
||||
|
||||
@@ -87,9 +87,9 @@ qthelp:
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/caravel.qhcp"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/superset.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/caravel.qhc"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/superset.qhc"
|
||||
|
||||
applehelp:
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@@ -104,8 +104,8 @@ devhelp:
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/caravel"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/caravel"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/superset"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/superset"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
|
||||
4
docs/_templates/layout.html
vendored
@@ -21,7 +21,7 @@
|
||||
<img src="_static/img/dash.png">
|
||||
<div class="carousel-caption">
|
||||
<div>
|
||||
<h1>Caravel</h1>
|
||||
<h1>Superset</h1>
|
||||
<p>
|
||||
an open source data visualization platform
|
||||
</p>
|
||||
@@ -80,7 +80,7 @@
|
||||
<hr/>
|
||||
<div class="container">
|
||||
<div class="jumbotron">
|
||||
<h1>Caravel</h1>
|
||||
<h1>Superset</h1>
|
||||
<p>
|
||||
is an open source data visualization platform that provides easy
|
||||
exploration of your data and allows you to create and share
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
rm -rf _build
|
||||
make html
|
||||
#cp -r ../caravel/assets/images/ _build/html/_static/img/
|
||||
cp -r ../caravel/assets/images/ _static/img/
|
||||
rm -rf /tmp/caravel-docs
|
||||
cp -r _build/html /tmp/caravel-docs
|
||||
#cp -r ../superset/assets/images/ _build/html/_static/img/
|
||||
cp -r ../superset/assets/images/ _static/img/
|
||||
rm -rf /tmp/superset-docs
|
||||
cp -r _build/html /tmp/superset-docs
|
||||
|
||||
30
docs/conf.py
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# caravel documentation build configuration file, created by
|
||||
# superset documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu Dec 17 15:42:06 2015.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
@@ -15,7 +15,7 @@
|
||||
import sys
|
||||
import os
|
||||
import shlex
|
||||
import sphinx_bootstrap_theme
|
||||
import sphinx_rtd_theme
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
@@ -51,8 +51,8 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'caravel'
|
||||
copyright = u'2015, Maxime Beauchemin, Airbnb'
|
||||
project = "Superset's documentation"
|
||||
copyright = None
|
||||
author = u'Maxime Beauchemin'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
@@ -113,19 +113,15 @@ todo_include_todos = False
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'bootstrap'
|
||||
html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {
|
||||
# 'bootswatch_theme': 'cosmo',
|
||||
'navbar_title': 'Caravel Documentation',
|
||||
'navbar_fixed_top': "false",
|
||||
'navbar_sidebarrel': False,
|
||||
'navbar_site_name': "Topics",
|
||||
#'navbar_class': "navbar navbar-left",
|
||||
'collapse_navigation': False,
|
||||
'display_version': False,
|
||||
}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
@@ -213,7 +209,7 @@ html_show_copyright = False
|
||||
#html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'caraveldoc'
|
||||
htmlhelp_basename = 'supersetdoc'
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
@@ -235,7 +231,7 @@ latex_elements = {
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, 'caravel.tex', u'Caravel Documentation',
|
||||
(master_doc, 'superset.tex', u'Superset Documentation',
|
||||
u'Maxime Beauchemin', 'manual'),
|
||||
]
|
||||
|
||||
@@ -265,7 +261,7 @@ latex_documents = [
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
(master_doc, 'Caravel', u'caravel Documentation',
|
||||
(master_doc, 'Superset', u'superset Documentation',
|
||||
[author], 1)
|
||||
]
|
||||
|
||||
@@ -279,8 +275,8 @@ man_pages = [
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(master_doc, 'Caravel', u'Caravel Documentation',
|
||||
author, 'Caravel', 'One line description of project.',
|
||||
(master_doc, 'Superset', u'Superset Documentation',
|
||||
author, 'Superset', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
|
||||
48
docs/druid.rst
Normal file
@@ -0,0 +1,48 @@
|
||||
Druid
|
||||
=====
|
||||
|
||||
Superset works well with Druid, though currently not all
|
||||
advanced features out of Druid are covered. This page clarifies what is
|
||||
covered and what isn't and explains how to use some of the features.
|
||||
|
||||
.. note ::
|
||||
Currently Airbnb runs against Druid ``0.8.x`` and previous /
|
||||
following versions are not tested against.
|
||||
|
||||
Supported
|
||||
'''''''''
|
||||
|
||||
Aggregations
|
||||
------------
|
||||
|
||||
Common aggregations, or Druid metrics can be defined and used in Superset.
|
||||
The first and simpler use case is to use the checkbox matrix expose in your
|
||||
datasource's edit view (``Sources -> Druid Datasources ->
|
||||
[your datasource] -> Edit -> [tab] List Druid Column``).
|
||||
Clicking the ``GroupBy`` and ``Filterable`` checkboxes will make the column
|
||||
appear in the related dropdowns while in explore view. Checking
|
||||
``Count Distinct``, ``Min``, ``Max`` or ``Sum`` will result in creating
|
||||
new metrics that will appear in the ``List Druid Metric`` tab upon saving the
|
||||
datasource. By editing these metrics, you'll notice that they their ``json``
|
||||
element correspond to Druid aggregation definition. You can create your own
|
||||
aggregations manually from the ``List Druid Metric`` tab following Druid
|
||||
documentation.
|
||||
|
||||
.. image:: _static/img/druid_agg.png
|
||||
:scale: 50 %
|
||||
|
||||
Post-Aggregations
|
||||
-----------------
|
||||
|
||||
Druid supports post aggregation and this works in Superset. All you have to
|
||||
do is creating a metric, much like you would create an aggregation manually,
|
||||
but specify ``postagg`` as a ``Metric Type``. You then have to provide a valid
|
||||
json post-aggregation definition (as specified in the Druid docs) in the
|
||||
Json field.
|
||||
|
||||
|
||||
Not yet supported
|
||||
'''''''''''''''''
|
||||
|
||||
- Regex filters
|
||||
- Lookups / joins
|
||||
95
docs/faq.rst
@@ -4,7 +4,7 @@ FAQ
|
||||
|
||||
Can I query/join multiple tables at one time?
|
||||
---------------------------------------------
|
||||
Not directly no. A Caravel SQLAlchemy datasource can only be a single table
|
||||
Not directly no. A Superset SQLAlchemy datasource can only be a single table
|
||||
or a view.
|
||||
|
||||
When working with tables, the solution would be to materialize
|
||||
@@ -14,15 +14,15 @@ through some scheduled batch process.
|
||||
A view is a simple logical layer that abstract an arbitrary SQL queries as
|
||||
a virtual table. This can allow you to join and union multiple tables, and
|
||||
to apply some transformation using arbitrary SQL expressions. The limitation
|
||||
there is your database performance as Caravel effectively will run a query
|
||||
there is your database performance as Superset effectively will run a query
|
||||
on top of your query (view). A good practice may be to limit yourself to
|
||||
joining your main large table to one or many small tables only, and avoid
|
||||
using ``GROUP BY`` where possible as Caravel will do its own ``GROUP BY`` and
|
||||
using ``GROUP BY`` where possible as Superset will do its own ``GROUP BY`` and
|
||||
doing the work twice might slow down performance.
|
||||
|
||||
Whether you use a table or a view, the important factor is whether your
|
||||
database is fast enough to serve it in an interactive fashion to provide
|
||||
a good user experience in Caravel.
|
||||
a good user experience in Superset.
|
||||
|
||||
|
||||
How BIG can my data source be?
|
||||
@@ -32,3 +32,90 @@ It can be gigantic! As mentioned above, the main criteria is whether your
|
||||
database can execute queries and return results in a time frame that is
|
||||
acceptable to your users. Many distributed databases out there can execute
|
||||
queries that scan through terabytes in an interactive fashion.
|
||||
|
||||
|
||||
How do I create my own visualization?
|
||||
-------------------------------------
|
||||
|
||||
We are planning on making it easier to add new visualizations to the
|
||||
framework, in the meantime, we've tagged a few pull requests as
|
||||
``example`` to give people examples of how to contribute new
|
||||
visualizations.
|
||||
|
||||
https://github.com/airbnb/superset/issues?q=label%3Aexample+is%3Aclosed
|
||||
|
||||
|
||||
Why are my queries timing out?
|
||||
------------------------------
|
||||
|
||||
If you are seeing timeouts (504 Gateway Time-out) when running queries,
|
||||
it's because the web server is timing out web requests. If you want to
|
||||
increase the default (50), you can specify the timeout when starting the
|
||||
web server with the ``-t`` flag, which is expressed in seconds.
|
||||
|
||||
``superset runserver -t 300``
|
||||
|
||||
|
||||
Why is the map not visible in the mapbox visualization?
|
||||
-------------------------------------------------------
|
||||
|
||||
You need to register to mapbox.com, get an API key and configure it as
|
||||
``MAPBOX_API_KEY`` in ``superset_config.py``.
|
||||
|
||||
|
||||
How to add dynamic filters to a dashboard?
|
||||
------------------------------------------
|
||||
|
||||
It's easy: use the ``Filter Box`` widget, build a slice, and add it to your
|
||||
dashboard.
|
||||
|
||||
The ``Filter Box`` widget allows you to define a query to populate dropdowns
|
||||
that can be use for filtering. To build the list of distinct values, we
|
||||
run a query, and sort the result by the metric you provide, sorting
|
||||
descending.
|
||||
|
||||
The widget also has a checkbox ``Date Filter``, which enables time filtering
|
||||
capabilities to your dashboard. After checking the box and refreshing, you'll
|
||||
see a ``from`` and a ``to`` dropdown show up.
|
||||
|
||||
But what about if you don't want certain widgets to get filtered on your
|
||||
dashboard? You can do that by editing your dashboard, and in the form,
|
||||
edit the ``JSON Metadata`` field, more specifically the
|
||||
``filter_immune_slices`` key, that receives an array of sliceIds that should
|
||||
never be affected by any dashboard level filtering.
|
||||
|
||||
|
||||
..code::
|
||||
|
||||
{
|
||||
"filter_immune_slices": [324, 65, 92],
|
||||
"expanded_slices": {},
|
||||
"filter_immune_slice_fields": {
|
||||
"177": ["country_name", "__from", "__to"],
|
||||
"32": ["__from", "__to"]
|
||||
}
|
||||
}
|
||||
|
||||
In the json blob above, slices 324, 65 and 92 won't be affected by any
|
||||
dashboard level filtering.
|
||||
|
||||
Now note the ``filter_immune_slice_fields`` key. This one allows you to
|
||||
be more specific and define for a specific slice_id, which filter fields
|
||||
should be disregarded.
|
||||
|
||||
Note the use of the ``__from`` and ``__to`` keywords, those are reserved
|
||||
for dealing with the time boundary filtering mentioned above.
|
||||
|
||||
But what happens with filtering when dealing with slices coming from
|
||||
different tables or databases? If the column name is shared, the filter will
|
||||
be applied, it's as simple as that.
|
||||
|
||||
Why does fabmanager or superset freezed/hung/not responding when started (my home directory is NFS mounted)?
|
||||
-----------------------------------------------------------------------------------------
|
||||
superset creates and uses an sqlite database at ``~/.superset/superset.db``. Sqlite is known to `don't work well if used on NFS`__ due to broken file locking implementation on NFS.
|
||||
|
||||
__ https://www.sqlite.org/lockingv3.html
|
||||
|
||||
One work around is to create a symlink from ~/.superset to a directory located on a non-NFS partition.
|
||||
|
||||
Another work around is to change where superset stores the sqlite database by adding ``SQLALCHEMY_DATABASE_URI = 'sqlite:////new/localtion/superset.db'`` in superset_config.py (create the file if needed), then adding the directory where superset_config.py lives to PYTHONPATH environment variable (e.g. ``export PYTHONPATH=/opt/logs/sandbox/airbnb/``).
|
||||
|
||||
@@ -70,3 +70,17 @@ Gallery
|
||||
.. image:: _static/img/viz_thumbnails/treemap.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/cal_heatmap.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/horizon.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/mapbox.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/separator.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/histogram.png
|
||||
:scale: 25 %
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
.. image:: _static/img/caravel.jpg
|
||||
Superset's documentation
|
||||
''''''''''''''''''''''''
|
||||
|
||||
.. warning:: This project used to be name Panoramix and has been renamed
|
||||
to Caravel in March 2016
|
||||
Superset is a data exploration platform designed to be visual, intuitive
|
||||
and interactive.
|
||||
|
||||
----------------
|
||||
|
||||
.. warning:: This project was originally named Panoramix, was renamed to
|
||||
Caravel in March 2016, and is currently named Superset as of November 2016
|
||||
|
||||
Overview
|
||||
=======================================
|
||||
@@ -24,6 +30,21 @@ Features
|
||||
- Integration with most RDBMS through SqlAlchemy
|
||||
- Deep integration with Druid.io
|
||||
|
||||
------
|
||||
|
||||
.. image:: https://camo.githubusercontent.com/82e264ef777ba06e1858766fe3b8817ee108eb7e/687474703a2f2f672e7265636f726469742e636f2f784658537661475574732e676966
|
||||
|
||||
------
|
||||
|
||||
.. image:: https://camo.githubusercontent.com/4991ff37a0005ea4e4267919a52786fda82d2d21/687474703a2f2f672e7265636f726469742e636f2f755a6767594f645235672e676966
|
||||
|
||||
------
|
||||
|
||||
.. image:: https://camo.githubusercontent.com/a389af15ac1e32a3d0fee941b4c62c850b1d583b/687474703a2f2f672e7265636f726469742e636f2f55373046574c704c76682e676966
|
||||
|
||||
------
|
||||
|
||||
|
||||
Contents
|
||||
---------
|
||||
|
||||
@@ -32,8 +53,11 @@ Contents
|
||||
|
||||
installation
|
||||
tutorial
|
||||
security
|
||||
sqllab
|
||||
videos
|
||||
gallery
|
||||
druid
|
||||
faq
|
||||
|
||||
|
||||
|
||||
@@ -4,19 +4,19 @@ Installation & Configuration
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
Caravel is currently only tested using Python 2.7.*. Python 3 support is
|
||||
on the roadmap, Python 2.6 won't be supported.
|
||||
Superset is tested using Python 2.7 and Python 3.4+. Python 3 is the recommended version,
|
||||
Python 2.6 won't be supported.
|
||||
|
||||
|
||||
OS dependencies
|
||||
---------------
|
||||
|
||||
Caravel stores database connection information in its metadata database.
|
||||
Superset stores database connection information in its metadata database.
|
||||
For that purpose, we use the ``cryptography`` Python library to encrypt
|
||||
connection passwords. Unfortunately this library has OS level dependencies.
|
||||
|
||||
You may want to attempt the next step
|
||||
("Caravel installation and initialization") and come back to this step if
|
||||
("Superset installation and initialization") and come back to this step if
|
||||
you encounter an error.
|
||||
|
||||
Here's how to install them:
|
||||
@@ -24,13 +24,13 @@ Here's how to install them:
|
||||
For **Debian** and **Ubuntu**, the following command will ensure that
|
||||
the required dependencies are installed: ::
|
||||
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip libsasl2-dev libldap2-dev
|
||||
|
||||
For **Fedora** and **RHEL-derivatives**, the following command will ensure
|
||||
that the required dependencies are installed: ::
|
||||
|
||||
|
||||
sudo yum upgrade python-setuptools
|
||||
sudo yum install gcc libffi-devel python-devel python-pip python-wheel openssl-devel
|
||||
sudo yum install gcc libffi-devel python-devel python-pip python-wheel openssl-devel libsasl2-devel openldap-devel
|
||||
|
||||
**OSX**, system python is not recommended. brew's python also ships with pip ::
|
||||
|
||||
@@ -40,60 +40,105 @@ that the required dependencies are installed: ::
|
||||
**Windows** isn't officially supported at this point, but if you want to
|
||||
attempt it, download `get-pip.py <https://bootstrap.pypa.io/get-pip.py>`_, and run ``python get-pip.py`` which may need admin access. Then run the following: ::
|
||||
|
||||
C:\> \path\to\vcvarsall.bat x86_amd64
|
||||
C:\> set LIB=C:\OpenSSL-1.0.1f-64bit\lib;%LIB%
|
||||
C:\> set INCLUDE=C:\OpenSSL-1.0.1f-64bit\include;%INCLUDE%
|
||||
C:\> pip install cryptography
|
||||
|
||||
# You may also have to create C:\Temp
|
||||
C:\> md C:\Temp
|
||||
|
||||
Python virtualenv
|
||||
-----------------
|
||||
It is recommended to install Superset inside a virtualenv. Python 3 already ships virtualenv, for
|
||||
Python 2 you need to install it. If it's packaged for your operating systems install it from there
|
||||
otherwise you can install from pip: ::
|
||||
|
||||
Caravel installation and initialization
|
||||
---------------------------------------
|
||||
Follow these few simple steps to install Caravel.::
|
||||
pip install virtualenv
|
||||
|
||||
# Install caravel
|
||||
pip install caravel
|
||||
You can create and activate a virtualenv by: ::
|
||||
|
||||
# virtualenv is shipped in Python 3 as pyvenv
|
||||
virtualenv venv
|
||||
. ./venv/bin/activate
|
||||
|
||||
On windows the syntax for activating it is a bit different: ::
|
||||
|
||||
venv\Scripts\activate
|
||||
|
||||
Once you activated your virtualenv everything you are doing is confined inside the virtualenv.
|
||||
To exit a virtualenv just type ``deactivate``.
|
||||
|
||||
Python's setup tools and pip
|
||||
----------------------------
|
||||
Put all the chances on your side by getting the very latest ``pip``
|
||||
and ``setuptools`` libraries.::
|
||||
|
||||
pip install --upgrade setuptools pip
|
||||
|
||||
Superset installation and initialization
|
||||
----------------------------------------
|
||||
Follow these few simple steps to install Superset.::
|
||||
|
||||
# Install superset
|
||||
pip install superset
|
||||
|
||||
# Create an admin user
|
||||
fabmanager create-admin --app caravel
|
||||
fabmanager create-admin --app superset
|
||||
|
||||
# Initialize the database
|
||||
caravel db upgrade
|
||||
|
||||
# Create default roles and permissions
|
||||
caravel init
|
||||
superset db upgrade
|
||||
|
||||
# Load some data to play with
|
||||
caravel load_examples
|
||||
superset load_examples
|
||||
|
||||
# Start the development web server
|
||||
caravel runserver -d
|
||||
# Create default roles and permissions
|
||||
superset init
|
||||
|
||||
# Start the web server on port 8088
|
||||
superset runserver -p 8088
|
||||
|
||||
# To start a development web server, use the -d switch
|
||||
# superset runserver -d
|
||||
|
||||
|
||||
After installation, you should be able to point your browser to the right
|
||||
hostname:port `http://localhost:8088 <http://localhost:8088>`_, login using
|
||||
the credential you entered while creating the admin account, and navigate to
|
||||
`Menu -> Admin -> Refresh Metadata`. This action should bring in all of
|
||||
your datasources for Caravel to be aware of, and they should show up in
|
||||
your datasources for Superset to be aware of, and they should show up in
|
||||
`Menu -> Datasources`, from where you can start playing with your data!
|
||||
|
||||
Please note that *gunicorn*, Superset default application server, does not
|
||||
work on Windows so you need to use the development web server.
|
||||
The development web server though is not intended to be used on production systems
|
||||
so better use a supported platform that can run *gunicorn*.
|
||||
|
||||
Configuration behind a load balancer
|
||||
------------------------------------
|
||||
|
||||
If you are running superset behind a load balancer or reverse proxy (e.g. NGINX
|
||||
or ELB on AWS), you may need to utilise a healthcheck endpoint so that your
|
||||
load balancer knows if your superset instance is running. This is provided
|
||||
at ``/health`` which will return a 200 response containing "OK" if the
|
||||
webserver is running.
|
||||
|
||||
If the load balancer is inserting X-Forwarded-For/X-Forwarded-Proto headers, you
|
||||
should set `ENABLE_PROXY_FIX = True` in the superset config file to extract and use
|
||||
the headers.
|
||||
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
|
||||
To configure your application, you need to create a file (module)
|
||||
``caravel_config.py`` and make sure it is in your PYTHONPATH. Here are some
|
||||
``superset_config.py`` and make sure it is in your PYTHONPATH. Here are some
|
||||
of the parameters you can copy / paste in that configuration module: ::
|
||||
|
||||
#---------------------------------------------------------
|
||||
# Caravel specifix config
|
||||
# Superset specific config
|
||||
#---------------------------------------------------------
|
||||
ROW_LIMIT = 5000
|
||||
WEBSERVER_THREADS = 8
|
||||
SUPERSET_WORKERS = 4
|
||||
|
||||
CARAVEL_WEBSERVER_PORT = 8088
|
||||
SUPERSET_WEBSERVER_PORT = 8088
|
||||
#---------------------------------------------------------
|
||||
|
||||
#---------------------------------------------------------
|
||||
@@ -104,28 +149,36 @@ of the parameters you can copy / paste in that configuration module: ::
|
||||
|
||||
# The SQLAlchemy connection string to your database backend
|
||||
# This connection defines the path to the database that stores your
|
||||
# caravel metadata (slices, connections, tables, dashboards, ...).
|
||||
# superset metadata (slices, connections, tables, dashboards, ...).
|
||||
# Note that the connection information to connect to the datasources
|
||||
# you want to explore are managed directly in the web UI
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/caravel.db'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////path/to/superset.db'
|
||||
|
||||
# Flask-WTF flag for CSRF
|
||||
CSRF_ENABLED = True
|
||||
|
||||
# Set this API key to enable Mapbox visualizations
|
||||
MAPBOX_API_KEY = ''
|
||||
|
||||
This file also allows you to define configuration parameters used by
|
||||
Flask App Builder, the web framework used by Caravel. Please consult
|
||||
Flask App Builder, the web framework used by Superset. Please consult
|
||||
the `Flask App Builder Documentation
|
||||
<http://flask-appbuilder.readthedocs.org/en/latest/config.html>`_
|
||||
for more information on how to configure Caravel.
|
||||
for more information on how to configure Superset.
|
||||
|
||||
Please make sure to change:
|
||||
|
||||
* *SQLALCHEMY_DATABASE_URI*, by default it is stored at *~/.superset/superset.db*
|
||||
* *SECRET_KEY*, to a long random string
|
||||
|
||||
Database dependencies
|
||||
---------------------
|
||||
|
||||
Caravel does not ship bundled with connectivity to databases, except
|
||||
Superset does not ship bundled with connectivity to databases, except
|
||||
for Sqlite, which is part of the Python standard library.
|
||||
You'll need to install the required packages for the database you
|
||||
want to use as your metadata database as well as the packages needed to
|
||||
connect to the databases you want to access through Caravel.
|
||||
connect to the databases you want to access through Superset.
|
||||
|
||||
Here's a list of some of the recommended packages.
|
||||
|
||||
@@ -146,6 +199,12 @@ Here's a list of some of the recommended packages.
|
||||
+---------------+-------------------------------------+-------------------------------------------------+
|
||||
| MSSQL | ``pip install pymssql`` | ``mssql://`` |
|
||||
+---------------+-------------------------------------+-------------------------------------------------+
|
||||
| Impala | ``pip install impyla`` | ``impala://`` |
|
||||
+---------------+-------------------------------------+-------------------------------------------------+
|
||||
| SparkSQL | ``pip install pyhive`` | ``jdbc+hive://`` |
|
||||
+---------------+-------------------------------------+-------------------------------------------------+
|
||||
| Greenplum | ``pip install psycopg2`` | ``postgresql+psycopg2://`` |
|
||||
+---------------+-------------------------------------+-------------------------------------------------+
|
||||
|
||||
Note that many other database are supported, the main criteria being the
|
||||
existence of a functional SqlAlchemy dialect and Python driver. Googling
|
||||
@@ -156,15 +215,18 @@ database you want to connect to should get you to the right place.
|
||||
Caching
|
||||
-------
|
||||
|
||||
Caravel uses `Flask-Cache <https://pythonhosted.org/Flask-Cache/>`_ for
|
||||
Superset uses `Flask-Cache <https://pythonhosted.org/Flask-Cache/>`_ for
|
||||
caching purpose. Configuring your caching backend is as easy as providing
|
||||
a ``CACHE_CONFIG``, constant in your ``caravel_config.py`` that
|
||||
a ``CACHE_CONFIG``, constant in your ``superset_config.py`` that
|
||||
complies with the Flask-Cache specifications.
|
||||
|
||||
Flask-Cache supports multiple caching backends (Redis, Memcache,
|
||||
SimpleCache (in-memory), or the local filesystem).
|
||||
Flask-Cache supports multiple caching backends (Redis, Memcached,
|
||||
SimpleCache (in-memory), or the local filesystem). If you are going to use
|
||||
Memcached please use the pylibmc client library as python-memcached does
|
||||
not handle storing binary data correctly. If you use Redis, please install
|
||||
[python-redis](https://pypi.python.org/pypi/redis).
|
||||
|
||||
For setting your timeouts, this is done in the Caravel metadata and goes
|
||||
For setting your timeouts, this is done in the Superset metadata and goes
|
||||
up the "timeout searchpath", from your slice configuration, to your
|
||||
data source's configuration, to your database's and ultimately falls back
|
||||
into your global default defined in ``CACHE_CONFIG``.
|
||||
@@ -192,7 +254,7 @@ Schemas (Postgres & Redshift)
|
||||
|
||||
Postgres and Redshift, as well as other database,
|
||||
use the concept of **schema** as a logical entity
|
||||
on top of the **database**. For Caravel to connect to a specific schema,
|
||||
on top of the **database**. For Superset to connect to a specific schema,
|
||||
there's a **schema** parameter you can set in the table form.
|
||||
|
||||
|
||||
@@ -224,14 +286,69 @@ Druid
|
||||
|
||||
* Navigate to your datasources
|
||||
|
||||
Note that you can run the ``caravel refresh_druid`` command to refresh the
|
||||
Note that you can run the ``superset refresh_druid`` command to refresh the
|
||||
metadata from your Druid cluster(s)
|
||||
|
||||
|
||||
CORS
|
||||
-----
|
||||
|
||||
The extra CORS Dependency must be installed:
|
||||
|
||||
superset[cors]
|
||||
|
||||
|
||||
The following keys in `superset_config.py` can be specified to configure CORS:
|
||||
|
||||
|
||||
* ``ENABLE_CORS``: Must be set to True in order to enable CORS
|
||||
* ``CORS_OPTIONS``: options passed to Flask-CORS (`documentation <http://flask-cors.corydolphin.com/en/latest/api.html#extension>`)
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Upgrading should be as straightforward as running::
|
||||
|
||||
pip install caravel --upgrade
|
||||
caravel db upgrade
|
||||
pip install superset --upgrade
|
||||
superset db upgrade
|
||||
superset init
|
||||
|
||||
SQL Lab
|
||||
-------
|
||||
SQL Lab is a powerful SQL IDE that works with all SQLAlchemy compatible
|
||||
databases out there. By default, queries are run in a web request, and
|
||||
may eventually timeout as queries exceed the maximum duration of a web
|
||||
request in your environment, whether it'd be a reverse proxy or the Superset
|
||||
server itself.
|
||||
|
||||
In the modern analytics world, it's not uncommon to run large queries that
|
||||
run for minutes or hours.
|
||||
To enable support for long running queries that
|
||||
execute beyond the typical web request's timeout (30-60 seconds), it is
|
||||
necessary to deploy an asynchronous backend, which consist of one or many
|
||||
Superset worker, which is implemented as a Celery worker, and a Celery
|
||||
broker for which we recommend using Redis or RabbitMQ.
|
||||
|
||||
It's also preferable to setup an async result backend as a key value store
|
||||
that can hold the long-running query results for a period of time. More
|
||||
details to come as to how to set this up here soon.
|
||||
|
||||
SQL Lab supports templating in queries, and it's possible to override
|
||||
the default Jinja context in your environment by defining the
|
||||
``JINJA_CONTEXT_ADDONS`` in your superset configuration. Objects referenced
|
||||
in this dictionary are made available for users to use in their SQL.
|
||||
|
||||
|
||||
Making your own build
|
||||
---------------------
|
||||
|
||||
For more advanced users, you may want to build Superset from sources. That
|
||||
would be the case if you fork the project to add features specific to
|
||||
your environment.::
|
||||
|
||||
# assuming $SUPERSET_HOME as the root of the repo
|
||||
cd $SUPERSET_HOME/superset/assets
|
||||
npm install
|
||||
npm run prod
|
||||
cd $SUPERSET_HOME
|
||||
python setup.py install
|
||||
|
||||
150
docs/security.rst
Normal file
@@ -0,0 +1,150 @@
|
||||
Security
|
||||
========
|
||||
Security in Superset is handled by Flask AppBuilder (FAB). FAB is a
|
||||
"Simple and rapid application development framework, built on top of Flask.".
|
||||
FAB provides authentication, user management, permissions and roles.
|
||||
|
||||
|
||||
Provided Roles
|
||||
--------------
|
||||
Superset ships with a set of roles that are handled by Superset itself.
|
||||
You can assume that these roles will stay up-to-date as Superset evolves.
|
||||
Even though it's possible for ``Admin`` usrs to do so, it is not recommended
|
||||
that you alter these roles in any way by removing
|
||||
or adding permissions to them as these roles will be re-synchronized to
|
||||
their original values as you run your next ``superset init`` command.
|
||||
|
||||
Since it's not recommended to alter the roles described here, it's right
|
||||
to assume that your security strategy should be to compose user access based
|
||||
on these base roles and roles that you create. For instance you could
|
||||
create a role ``Financial Analyst`` that would be made of set of permissions
|
||||
to a set of data sources (tables) and/or databases. Users would then be
|
||||
granted ``Gamma``, ``Financial Analyst``, and perhaps ``sql_lab``.
|
||||
|
||||
Admin
|
||||
"""""
|
||||
Admins have all possible rights, including granting or revoking rights from
|
||||
other users and altering other people's slices and dashboards.
|
||||
|
||||
Alpha
|
||||
"""""
|
||||
Alpha have access to all data sources, but they cannot grant or revoke access
|
||||
from other users. They are also limited to altering the objects that they
|
||||
own. Alpha users can add and alter data sources.
|
||||
|
||||
Gamma
|
||||
"""""
|
||||
Gamma have limited access. They can only consume data coming from data sources
|
||||
they have been giving access to through another complementary role.
|
||||
They only have access to view the slices and
|
||||
dashboards made from data sources that they have access to. Currently Gamma
|
||||
users are not able to alter or add data sources. We assume that they are
|
||||
mostly content consumers, though they can create slices and dashboards.
|
||||
|
||||
Also note that when Gamma users look at the dashboards and slices list view,
|
||||
they will only see the objects that they have access to.
|
||||
|
||||
sql_lab
|
||||
"""""""
|
||||
The ``sql_lab`` role grants access to SQL Lab. Note that while ``Admin``
|
||||
users have access to all databases by default, both ``Alpha`` and ``Gamma``
|
||||
users need to be given access on a per database basis.
|
||||
|
||||
|
||||
Managing Gamma per data source access
|
||||
-------------------------------------
|
||||
Here's how to provide users access to only specific datasets. First make
|
||||
sure the users with limited access have [only] the Gamma role assigned to
|
||||
them. Second, create a new role (``Menu -> Security -> List Roles``) and
|
||||
click the ``+`` sign.
|
||||
|
||||
.. image:: _static/img/create_role.png
|
||||
:scale: 50 %
|
||||
|
||||
This new window allows you to give this new role a name, attribute it to users
|
||||
and select the tables in the ``Permissions`` dropdown. To select the data
|
||||
sources you want to associate with this role, simply click in the dropdown
|
||||
and use the typeahead to search for your table names.
|
||||
|
||||
You can then confirm with your Gamma users that they see the objects
|
||||
(dashboards and slices) associated with the tables related to their roles.
|
||||
|
||||
|
||||
Customizing
|
||||
-----------
|
||||
|
||||
The permissions exposed by FAB are very granular and allow for a great level
|
||||
of customization. FAB creates many permissions automagically for each model
|
||||
that is create (can_add, can_delete, can_show, can_edit, ...) as well as for
|
||||
each view. On top of that, Superset can expose more granular permissions like
|
||||
``all_datasource_access``.
|
||||
|
||||
We do not recommend altering the 3 base roles as there
|
||||
are a set of assumptions that Superset build upon. It is possible though for
|
||||
you to create your own roles, and union them to existing ones.
|
||||
|
||||
Permissions
|
||||
"""""""""""
|
||||
|
||||
Roles are composed of a set of permissions, and Superset has many categories
|
||||
of permissions. Here are the different categories of permissions:
|
||||
|
||||
- **Model & action**: models are entities like ``Dashboard``,
|
||||
``Slice``, or ``User``. Each model has a fixed set of permissions, like
|
||||
``can_edit``, ``can_show``, ``can_delete``, ``can_list``, ``can_add``, and
|
||||
so on. By adding ``can_delete on Dashboard`` to a role, and granting that
|
||||
role to a user, this user will be able to delete dashboards.
|
||||
- **Views**: views are individual web pages, like the ``explore`` view or the
|
||||
``SQL Lab`` view. When granted to a user, he/she will see that view in
|
||||
the its menu items, and be able to load that page.
|
||||
- **Data source**: For each data source, a permission is created. If the user
|
||||
does not have the ``all_datasource_access`` permission granted, the user
|
||||
will only be able to see Slices or explore the data sources that are granted
|
||||
to them
|
||||
- **Database**: Granting access to a database allows for the user to access
|
||||
all data sources within that database, and will enable the user to query
|
||||
that database in SQL Lab, provided that the SQL Lab specific permission
|
||||
have been granted to the user
|
||||
|
||||
|
||||
Restricting access to a subset of data sources
|
||||
""""""""""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
The best way to go is probably to give user ``Gamma`` plus one or many other
|
||||
roles that would add access to specific data sources. We recommend that you
|
||||
create individual roles for each access profile. Say people in your finance
|
||||
department might have access to a set of databases and data sources, and
|
||||
these permissions can be consolidated in a single role. Users with this
|
||||
profile then need to be attributed ``Gamma`` as a foundation to the models
|
||||
and views they can access, and that ``Finance`` role that is a collection
|
||||
of permissions to data objects.
|
||||
|
||||
One user can have many roles, so a finance executive could be granted
|
||||
``Gamma``, ``Finance``, and perhaps another ``Executive`` role that gather
|
||||
a set of data sources that power dashboards only made available to executives.
|
||||
When looking at its dashboard list, this user will only see the
|
||||
list of dashboards it has access to, based on the roles and
|
||||
permissions that were attributed.
|
||||
|
||||
|
||||
Restricting the access to some metrics
|
||||
""""""""""""""""""""""""""""""""""""""
|
||||
|
||||
Sometimes some metrics are relatively sensitive (e.g. revenue).
|
||||
We may want to restrict those metrics to only a few roles.
|
||||
For example, assumed there is a metric ``[cluster1].[datasource1].[revenue]``
|
||||
and only Admin users are allowed to see it. Here’s how to restrict the access.
|
||||
|
||||
1. Edit the datasource (``Menu -> Source -> Druid datasources -> edit the
|
||||
record "datasource1"``) and go to the tab ``List Druid Metric``. Check
|
||||
the checkbox ``Is Restricted`` in the row of the metric ``revenue``.
|
||||
|
||||
2. Edit the role (``Menu -> Security -> List Roles -> edit the record
|
||||
“Admin”``), in the permissions field, type-and-search the permission
|
||||
``metric access on [cluster1].[datasource1].[revenue] (id: 1)``, then
|
||||
click the Save button on the bottom of the page.
|
||||
|
||||
Any users without the permission will see the error message
|
||||
*Access to the metrics denied: revenue (Status: 500)* in the slices.
|
||||
It also happens when the user wants to access a post-aggregation metric that
|
||||
is dependent on revenue.
|
||||
60
docs/sqllab.rst
Normal file
@@ -0,0 +1,60 @@
|
||||
SQL Lab
|
||||
=======
|
||||
|
||||
SQL Lab is a modern, feature-rich SQL IDE written in
|
||||
`React <https://facebook.github.io/react/>`_.
|
||||
|
||||
|
||||
Feature Overview
|
||||
----------------
|
||||
- Connects to just about any database backend
|
||||
- A multi-tab environment to work on multiple queries at a time
|
||||
- A smooth flow to visualize your query results using Superset's rich
|
||||
visualization capabilities
|
||||
- Browse database metadata: tables, columns, indexes, partitions
|
||||
- Support for long-running queries
|
||||
- uses the `Celery distributed queue <http://www.python.org/>`_
|
||||
to dispatch query handling to workers
|
||||
- supports defining a "results backend" to persist query results
|
||||
- A search engine to find queries executed in the past
|
||||
- Supports templating using the
|
||||
`Jinja templating language <http://jinja.pocoo.org/docs/dev/>`_
|
||||
which allows for using macros in your SQL code
|
||||
|
||||
Extra features
|
||||
--------------
|
||||
- Hit ``alt + enter`` as a keyboard shortcut to run your query
|
||||
|
||||
Templating with Jinja
|
||||
---------------------
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT *
|
||||
FROM some_table
|
||||
WHERE partition_key = '{{ presto.latest_partition('some_table') }}'
|
||||
|
||||
Templating unleashes the power and capabilities of a
|
||||
programming language within your SQL code.
|
||||
|
||||
Templates can also be used to write generic queries that are
|
||||
parameterized so they can be re-used easily.
|
||||
|
||||
|
||||
Available macros
|
||||
''''''''''''''''
|
||||
|
||||
We expose certain modules from Python's standard library in
|
||||
Superset's Jinja context:
|
||||
- ``time``: ``time``
|
||||
- ``datetime``: ``datetime.datetime``
|
||||
- ``uuid``: ``uuid``
|
||||
- ``random``: ``random``
|
||||
- ``relativedelta``: ``dateutil.relativedelta.relativedelta``
|
||||
- more to come!
|
||||
|
||||
`Jinja's builtin filters <http://jinja.pocoo.org/docs/dev/templates/>`_ can be also be applied where needed.
|
||||
|
||||
|
||||
.. autoclass:: superset.jinja_context.PrestoTemplateProcessor
|
||||
:members:
|
||||
@@ -3,7 +3,7 @@ Tutorial
|
||||
|
||||
This basic linear tutorial will take you through connecting to a database,
|
||||
adding a table, creating a slice and a dashboard. First you'll need to tell
|
||||
Caravel where to find the database you want to
|
||||
Superset where to find the database you want to
|
||||
query. First go to the database menu
|
||||
|
||||
.. image:: _static/img/tutorial/db_menu.png
|
||||
@@ -29,7 +29,7 @@ plus (``+``) sign there (similar to the one ).
|
||||
|
||||
Now enter the name of the table in the ``Table Name`` textbox, and select
|
||||
the database you just created in the ``Database`` dropdown, hit save. At this
|
||||
moment, Caravel fetched the column names, their data types and tries to guess
|
||||
moment, Superset fetched the column names, their data types and tries to guess
|
||||
which fields are metrics in dimensions. From the list view, edit the table
|
||||
that you just created by clicking the tiny pen icon.
|
||||
|
||||
@@ -42,9 +42,9 @@ showing you the list of columns in your table as well as their data types.
|
||||
.. image:: _static/img/tutorial/matrix.png
|
||||
:scale: 30 %
|
||||
|
||||
Click the checkboxes here that inform Caravel how your columns should be
|
||||
Click the checkboxes here that inform Superset how your columns should be
|
||||
shown in the explore view, and which metrics should be created. Make sure
|
||||
to inform Caravel about your date columns. You could also create
|
||||
to inform Superset about your date columns. You could also create
|
||||
"SQL expression" columns here, or metrics in that tab as aggregate expressions,
|
||||
but let's not do that just yet. Hit ``save``.
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ Videos
|
||||
======
|
||||
|
||||
Here is a collection of short videos showing different aspect
|
||||
of Caravel.
|
||||
of Superset.
|
||||
|
||||
Quick Intro
|
||||
'''''''''''
|
||||
This video demonstrates how Caravel works at a high level, it shows how
|
||||
This video demonstrates how Superset works at a high level, it shows how
|
||||
to navigate through datasets and dashboards that are already available.
|
||||
|
||||
.. youtube:: https://www.youtube.com/watch?v=3Txm_nj_R7M
|
||||
@@ -41,7 +41,7 @@ to toggle them on dashboards.
|
||||
|
||||
Adding a Table
|
||||
''''''''''''''
|
||||
This videos shows you how to expose a new table in Caravel, and how to
|
||||
This videos shows you how to expose a new table in Superset, and how to
|
||||
define the semantics on how this can be accessed by others in the ``Explore``
|
||||
and ``Dashboard`` views.
|
||||
|
||||
|
||||
7
pypi_push.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
cd superset/assets/
|
||||
rm build/*
|
||||
npm run prod
|
||||
cd ../..
|
||||
python setup.py register
|
||||
python setup.py sdist upload
|
||||
|
||||
9
run_specific_test.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
echo $DB
|
||||
rm -f .coverage
|
||||
export SUPERSET_CONFIG=tests.superset_test_config
|
||||
set -e
|
||||
superset/bin/superset version -v
|
||||
export SOLO_TEST=1
|
||||
# e.g. tests.core_tests:CoreTests.test_templated_sql_json
|
||||
nosetests $1
|
||||
13
run_tests.sh
@@ -1,6 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
rm /tmp/caravel_unittests.db
|
||||
echo $DB
|
||||
rm ~/.superset/unittests.db
|
||||
rm ~/.superset/celerydb.sqlite
|
||||
rm ~/.superset/celery_results.sqlite
|
||||
rm -f .coverage
|
||||
export CARAVEL_CONFIG=tests.caravel_test_config
|
||||
caravel/bin/caravel db upgrade
|
||||
export SUPERSET_CONFIG=tests.superset_test_config
|
||||
set -e
|
||||
superset/bin/superset db upgrade
|
||||
superset/bin/superset db upgrade # running twice on purpose as a test
|
||||
superset/bin/superset version -v
|
||||
python setup.py nosetests
|
||||
coveralls
|
||||
|
||||
10
setup.cfg
@@ -1,5 +1,5 @@
|
||||
[metadata]
|
||||
name = Caravel
|
||||
name = Superset
|
||||
summary = a data exploration platform
|
||||
description-file = README.md
|
||||
author = Maxime Beauchemin
|
||||
@@ -7,7 +7,7 @@ author-email = maximebeauchemin@gmail.com
|
||||
license = Apache License, Version 2.0
|
||||
|
||||
[files]
|
||||
packages = caravel
|
||||
packages = superset
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = docs/
|
||||
@@ -21,4 +21,8 @@ upload-dir = docs/_build/html
|
||||
verbosity=3
|
||||
detailed-errors=1
|
||||
with-coverage=1
|
||||
cover-package=caravel
|
||||
nocapture=1
|
||||
cover-package=superset
|
||||
|
||||
[pycodestyle]
|
||||
max-line-length=90
|
||||
|
||||
79
setup.py
@@ -1,45 +1,68 @@
|
||||
import imp
|
||||
import os
|
||||
import json
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
VERSION = '0.8.8'
|
||||
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
PACKAGE_DIR = os.path.join(BASE_DIR, 'superset', 'static', 'assets')
|
||||
PACKAGE_FILE = os.path.join(PACKAGE_DIR, 'package.json')
|
||||
with open(PACKAGE_FILE) as package_file:
|
||||
version_string = json.load(package_file)['version']
|
||||
|
||||
setup(
|
||||
name='caravel',
|
||||
name='superset',
|
||||
description=(
|
||||
"A interactive data visualization platform build on SqlAlchemy "
|
||||
"and druid.io"),
|
||||
version=VERSION,
|
||||
version=version_string,
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
zip_safe=False,
|
||||
scripts=['caravel/bin/caravel'],
|
||||
scripts=['superset/bin/superset'],
|
||||
install_requires=[
|
||||
'alembic>=0.8.5, <0.9.0',
|
||||
'cryptography>=1.1.1, <2.0.0',
|
||||
'flask-appbuilder>=1.6.0, <2.0.0',
|
||||
'flask-cache>=0.13.1, <0.14.0',
|
||||
'flask-migrate>=1.5.1, <2.0.0',
|
||||
'flask-script>=2.0.5, <3.0.0',
|
||||
'flask-sqlalchemy==2.0.0',
|
||||
'flask-testing>=0.4.2, <0.5.0',
|
||||
'flask>=0.10.1, <1.0.0',
|
||||
'humanize>=0.5.1, <0.6.0',
|
||||
'gunicorn>=19.3.0, <20.0.0',
|
||||
'markdown>=2.6.2, <3.0.0',
|
||||
'pandas==0.18.0',
|
||||
'celery==3.1.23',
|
||||
'cryptography==1.5.3',
|
||||
'flask-appbuilder==1.8.1',
|
||||
'flask-cache==0.13.1',
|
||||
'flask-migrate==1.5.1',
|
||||
'flask-script==2.0.5',
|
||||
'flask-testing==0.5.0',
|
||||
'flask-sqlalchemy==2.0',
|
||||
'humanize==0.5.1',
|
||||
'gunicorn==19.6.0',
|
||||
'markdown==2.6.6',
|
||||
'pandas==0.18.1',
|
||||
'parsedatetime==2.0.0',
|
||||
'pydruid>=0.2.2, <0.3',
|
||||
'python-dateutil>=2.4.2, <3.0.0',
|
||||
'requests>=2.7.0, <3.0.0',
|
||||
'sqlalchemy>=1.0.12, <2.0.0',
|
||||
'sqlalchemy-utils>=0.31.3, <0.32.0',
|
||||
'sqlparse>=0.1.16, <0.2.0',
|
||||
'werkzeug>=0.11.2, <0.12.0',
|
||||
'pydruid==0.3.0',
|
||||
'PyHive>=0.2.1',
|
||||
'python-dateutil==2.5.3',
|
||||
'requests==2.10.0',
|
||||
'simplejson==3.8.2',
|
||||
'six==1.10.0',
|
||||
'sqlalchemy==1.0.13',
|
||||
'sqlalchemy-utils==0.32.7',
|
||||
'sqlparse==0.1.19',
|
||||
'thrift>=0.9.3',
|
||||
'thrift-sasl>=0.2.1',
|
||||
'werkzeug==0.11.10',
|
||||
],
|
||||
extras_require={
|
||||
'cors': ['Flask-Cors>=2.0.0'],
|
||||
},
|
||||
tests_require=[
|
||||
'codeclimate-test-reporter',
|
||||
'coverage',
|
||||
'mock',
|
||||
'nose',
|
||||
],
|
||||
tests_require=['coverage'],
|
||||
author='Maxime Beauchemin',
|
||||
author_email='maximebeauchemin@gmail.com',
|
||||
url='https://github.com/airbnb/caravel',
|
||||
url='https://github.com/airbnb/superset',
|
||||
download_url=(
|
||||
'https://github.com/airbnb/caravel/tarball/' + VERSION),
|
||||
'https://github.com/airbnb/superset/tarball/' + version_string),
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
],
|
||||
)
|
||||
|
||||
89
superset/__init__.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Package's main module!"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
|
||||
from flask import Flask, redirect
|
||||
from flask_appbuilder import SQLA, AppBuilder, IndexView
|
||||
from flask_appbuilder.baseviews import expose
|
||||
from flask_cache import Cache
|
||||
from flask_migrate import Migrate
|
||||
from superset.source_registry import SourceRegistry
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
from superset import utils
|
||||
|
||||
|
||||
APP_DIR = os.path.dirname(__file__)
|
||||
CONFIG_MODULE = os.environ.get('SUPERSET_CONFIG', 'superset.config')
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config.from_object(CONFIG_MODULE)
|
||||
conf = app.config
|
||||
|
||||
if not app.debug:
|
||||
# In production mode, add log handler to sys.stderr.
|
||||
app.logger.addHandler(logging.StreamHandler())
|
||||
app.logger.setLevel(logging.INFO)
|
||||
|
||||
db = SQLA(app)
|
||||
|
||||
|
||||
utils.pessimistic_connection_handling(db.engine.pool)
|
||||
|
||||
cache = Cache(app, config=app.config.get('CACHE_CONFIG'))
|
||||
|
||||
migrate = Migrate(app, db, directory=APP_DIR + "/migrations")
|
||||
|
||||
# Logging configuration
|
||||
logging.basicConfig(format=app.config.get('LOG_FORMAT'))
|
||||
logging.getLogger().setLevel(app.config.get('LOG_LEVEL'))
|
||||
|
||||
if app.config.get('ENABLE_TIME_ROTATE'):
|
||||
logging.getLogger().setLevel(app.config.get('TIME_ROTATE_LOG_LEVEL'))
|
||||
handler = TimedRotatingFileHandler(app.config.get('FILENAME'),
|
||||
when=app.config.get('ROLLOVER'),
|
||||
interval=app.config.get('INTERVAL'),
|
||||
backupCount=app.config.get('BACKUP_COUNT'))
|
||||
logging.getLogger().addHandler(handler)
|
||||
|
||||
if app.config.get('ENABLE_CORS'):
|
||||
from flask_cors import CORS
|
||||
CORS(app, **app.config.get('CORS_OPTIONS'))
|
||||
|
||||
if app.config.get('ENABLE_PROXY_FIX'):
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
|
||||
if app.config.get('UPLOAD_FOLDER'):
|
||||
try:
|
||||
os.makedirs(app.config.get('UPLOAD_FOLDER'))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class MyIndexView(IndexView):
|
||||
@expose('/')
|
||||
def index(self):
|
||||
return redirect('/superset/welcome')
|
||||
|
||||
appbuilder = AppBuilder(
|
||||
app, db.session,
|
||||
base_template='superset/base.html',
|
||||
indexview=MyIndexView,
|
||||
security_manager_class=app.config.get("CUSTOM_SECURITY_MANAGER"))
|
||||
|
||||
sm = appbuilder.sm
|
||||
|
||||
get_session = appbuilder.get_session
|
||||
results_backend = app.config.get("RESULTS_BACKEND")
|
||||
|
||||
# Registering sources
|
||||
module_datasource_map = app.config.get("DEFAULT_MODULE_DS_MAP")
|
||||
module_datasource_map.update(app.config.get("ADDITIONAL_MODULE_DS_MAP"))
|
||||
SourceRegistry.register_sources(module_datasource_map)
|
||||
|
||||
from superset import views, config # noqa
|
||||
3
superset/assets/.babelrc
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets" : ["airbnb", "es2015", "react"],
|
||||
}
|
||||
9
superset/assets/.eslintignore
Normal file
@@ -0,0 +1,9 @@
|
||||
**/*{.,-}min.js
|
||||
**/*.sh
|
||||
coverage/**
|
||||
dist/*
|
||||
images/*
|
||||
node_modules/*
|
||||
node_modules*/*
|
||||
stylesheets/*
|
||||
vendor/*
|
||||
18
superset/assets/.eslintrc
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"extends": "airbnb",
|
||||
"parserOptions":{
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"prefer-template": 0,
|
||||
"new-cap": 0,
|
||||
"no-restricted-syntax": 0,
|
||||
"guard-for-in": 0,
|
||||
"prefer-arrow-callback": 0,
|
||||
"func-names": 0,
|
||||
"react/jsx-no-bind": 0,
|
||||
"no-confusing-arrow": 0,
|
||||
}
|
||||
}
|
||||
54
superset/assets/.istanbul.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
verbose: true
|
||||
instrumentation:
|
||||
root: './javascripts'
|
||||
extensions: ['.js', '.jsx']
|
||||
excludes: [
|
||||
'dist/**'
|
||||
]
|
||||
embed-source: false
|
||||
variable: __coverage__
|
||||
compact: true
|
||||
preserve-comments: false
|
||||
complete-copy: false
|
||||
save-baseline: true
|
||||
baseline-file: ./coverage/coverage-baseline.json
|
||||
include-all-sources: true
|
||||
include-pid: false
|
||||
es-modules: true
|
||||
reporting:
|
||||
print: summary
|
||||
reports:
|
||||
- lcov
|
||||
dir: ./coverage
|
||||
watermarks:
|
||||
statements: [50, 80]
|
||||
lines: [50, 80]
|
||||
functions: [50, 80]
|
||||
branches: [50, 80]
|
||||
report-config:
|
||||
clover: {file: clover.xml}
|
||||
cobertura: {file: cobertura-coverage.xml}
|
||||
json: {file: coverage-final.json}
|
||||
json-summary: {file: coverage-summary.json}
|
||||
lcovonly: {file: lcov.info}
|
||||
teamcity: {file: null, blockName: Code Coverage Summary}
|
||||
text: {file: null, maxCols: 0}
|
||||
text-lcov: {file: lcov.info}
|
||||
text-summary: {file: null}
|
||||
hooks:
|
||||
hook-run-in-context: false
|
||||
post-require-hook: null
|
||||
handle-sigint: false
|
||||
check:
|
||||
global:
|
||||
statements: 0
|
||||
lines: 0
|
||||
branches: 0
|
||||
functions: 0
|
||||
excludes: []
|
||||
each:
|
||||
statements: 0
|
||||
lines: 0
|
||||
branches: 0
|
||||
functions: 0
|
||||
excludes: []
|
||||
BIN
superset/assets/images/babytux.jpg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |