Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5dead4791 | ||
|
|
d933a21216 | ||
|
|
59169bfc96 | ||
|
|
bcca840f01 | ||
|
|
90a3b9f2c4 | ||
|
|
a37e431150 | ||
|
|
bf38c714a5 | ||
|
|
6b0b03e009 | ||
|
|
8556b098f9 | ||
|
|
d122b37f5d | ||
|
|
1756c27930 | ||
|
|
7867267608 | ||
|
|
92d588694b | ||
|
|
d10eaeccc9 | ||
|
|
c2bb49fec5 | ||
|
|
062f2b81cf | ||
|
|
65e72d0d07 | ||
|
|
345727635e | ||
|
|
c2baa53b06 | ||
|
|
31758827ae | ||
|
|
81de51bf6f | ||
|
|
0d1f27dbc1 | ||
|
|
c7282882d5 | ||
|
|
f9d04e8a72 | ||
|
|
bf2e804331 | ||
|
|
c349b0a1c1 | ||
|
|
4d640b5a3d | ||
|
|
380c3f0c75 | ||
|
|
e3e8202c98 | ||
|
|
889844407f | ||
|
|
f1830c36cf | ||
|
|
92f73b67ca | ||
|
|
9c1af66ba4 | ||
|
|
2b31ab498b | ||
|
|
034fd077e1 | ||
|
|
ca4443247e | ||
|
|
6f96252e45 | ||
|
|
0b93fd373d | ||
|
|
c3789d53b4 | ||
|
|
aec3c0b358 | ||
|
|
ef45c20558 | ||
|
|
10ab678fc6 | ||
|
|
87fb40ae26 | ||
|
|
d245fb91b5 | ||
|
|
c60032acce | ||
|
|
d2f51900f1 | ||
|
|
93405dc23a | ||
|
|
dafdb51f35 | ||
|
|
2d0fdf7e59 |
1
.gitignore
vendored
@@ -4,6 +4,7 @@ babel
|
||||
.coverage
|
||||
_build
|
||||
_static
|
||||
_images
|
||||
caravel/bin/caravelc
|
||||
env_py3
|
||||
.eggs
|
||||
|
||||
@@ -6,13 +6,13 @@ cache:
|
||||
directories:
|
||||
- $HOME/.wheelhouse/
|
||||
before_install:
|
||||
- rm -rf ~/.npm
|
||||
- mkdir ~/.npm
|
||||
- npm install -g npm@'>=2.7.1'
|
||||
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
|
||||
- npm install
|
||||
- npm run lint
|
||||
- npm run prod
|
||||
|
||||
185
CHANGELOG.md
Normal file
@@ -0,0 +1,185 @@
|
||||
## Change Log
|
||||
|
||||
### 0.8.5 (2016/04/01 20:30 +00:00)
|
||||
- [#234](https://github.com/airbnb/caravel/pull/234) Pin pandas, remove numpy (@mistercrunch)
|
||||
- [#229](https://github.com/airbnb/caravel/pull/229) Remove "requirements.txt" mention from README (@jmcomets)
|
||||
- [#225](https://github.com/airbnb/caravel/pull/225) remove power units from sankey diagram (@williaster)
|
||||
- [#219](https://github.com/airbnb/caravel/pull/219) Add 'Percent of previous' to sunburst vis. Appease npm warnings for data tables and d3.layout.cloud (@williaster)
|
||||
- [#224](https://github.com/airbnb/caravel/pull/224) Fixing minor typos in the readme (@cyrusstoller)
|
||||
- [#214](https://github.com/airbnb/caravel/pull/214) Fix an installation bug. (@kim-pham-airbnb)
|
||||
- [#218](https://github.com/airbnb/caravel/pull/218) Redirecting URL from previous names to caravel (@mistercrunch)
|
||||
- [#223](https://github.com/airbnb/caravel/pull/223) Fixed typo in README (@thebucknerlife)
|
||||
- [#222](https://github.com/airbnb/caravel/pull/222) remove duplicate Druid.io section in README.md (@brchristian)
|
||||
- [#213](https://github.com/airbnb/caravel/pull/213) Fix a bug when loading DruidDatasource. (@kim-pham-airbnb)
|
||||
- [#204](https://github.com/airbnb/caravel/pull/204) Fixing the order and coverage report for the unit tests (@mistercrunch)
|
||||
- [#209](https://github.com/airbnb/caravel/pull/209) Fresh screenshots (@mistercrunch)
|
||||
- [#206](https://github.com/airbnb/caravel/pull/206) Caravel (@mistercrunch)
|
||||
- [#205](https://github.com/airbnb/caravel/pull/205) fix sunburst error. add `less` to package.json (@williaster)
|
||||
- [#203](https://github.com/airbnb/caravel/pull/203) Fixing mysql install (@mistercrunch)
|
||||
- [#202](https://github.com/airbnb/caravel/pull/202) Using setup.py nosetests to run tests (@mistercrunch)
|
||||
- [#199](https://github.com/airbnb/caravel/pull/199) Fix a few minor bugs (@mistercrunch)
|
||||
- [#200](https://github.com/airbnb/caravel/pull/200) Add a sankey example (@mistercrunch)
|
||||
- [#192](https://github.com/airbnb/caravel/pull/192) Fix Druid metadata refresh. (@kim-pham-airbnb)
|
||||
- [#198](https://github.com/airbnb/caravel/pull/198) A welcome page (@mistercrunch)
|
||||
- [#197](https://github.com/airbnb/caravel/pull/197) Adding a DRUID_IS_ACTIVE flag and changing nav bar (@NiharikaRay)
|
||||
- [#196](https://github.com/airbnb/caravel/pull/196) Fixing issues around fk nullable=False on audit fields (@mistercrunch)
|
||||
|
||||
### 0.8.4 (2016/03/24 05:26 +00:00)
|
||||
- [#193](https://github.com/airbnb/caravel/pull/193) Adding favorites for Slices and Dashboards (@mistercrunch)
|
||||
|
||||
### 0.8.2 (2016/03/23 20:43 +00:00)
|
||||
- [#188](https://github.com/airbnb/caravel/pull/188) Introducing a caching layer! (@mistercrunch)
|
||||
|
||||
### 0.8.1 (2016/03/21 23:41 +00:00)
|
||||
- [#191](https://github.com/airbnb/caravel/pull/191) Add week ending and week start to grain (@airbnb)
|
||||
- [#190](https://github.com/airbnb/caravel/pull/190) Cranking up version numbers (@mistercrunch)
|
||||
- [#184](https://github.com/airbnb/caravel/pull/184) sunburst improvements (@williaster)
|
||||
- [#186](https://github.com/airbnb/caravel/pull/186) Adding docstrings ! (@mistercrunch)
|
||||
- [#181](https://github.com/airbnb/caravel/pull/181) Dynamic time granularity on any datetime column (@mistercrunch)
|
||||
- [#182](https://github.com/airbnb/caravel/pull/182) more css fixes (@williaster)
|
||||
- [#178](https://github.com/airbnb/caravel/pull/178) Allowing all extra fields in AuditMixin to be nullable (@mistercrunch)
|
||||
- [#175](https://github.com/airbnb/caravel/pull/175) refactor dashboard chart html, make several css improvements. (@williaster)
|
||||
|
||||
### 0.8.0 (2016/03/11 03:33 +00:00)
|
||||
- [#172](https://github.com/airbnb/caravel/pull/172) Fixing the python and js packaging (@mistercrunch)
|
||||
- [#171](https://github.com/airbnb/caravel/pull/171) Fixing multiple refresh bug in filter_box (@mistercrunch)
|
||||
- [#169](https://github.com/airbnb/caravel/pull/169) Fixing the look of select2 components (@mistercrunch)
|
||||
- [#168](https://github.com/airbnb/caravel/pull/168) Getting travis to build the npm related stuff (@mistercrunch)
|
||||
- [#166](https://github.com/airbnb/caravel/pull/166) make css theme customization easier by using less for bootstrap themes (@williaster)
|
||||
- [#163](https://github.com/airbnb/caravel/pull/163) Shipping with CSS templates out of the box (@mistercrunch)
|
||||
- [#164](https://github.com/airbnb/caravel/pull/164) Improving the docs (@mistercrunch)
|
||||
- [#165](https://github.com/airbnb/caravel/pull/165) Fixing window resize for explore and standalone (@mistercrunch)
|
||||
- [#161](https://github.com/airbnb/caravel/pull/161) Add linting to package.json, do all of the linting. (@williaster)
|
||||
- [#160](https://github.com/airbnb/caravel/pull/160) Fixing the dashed line when using time compare (@mistercrunch)
|
||||
- [#159](https://github.com/airbnb/caravel/pull/159) Fixing the standalone mode (@mistercrunch)
|
||||
- [#158](https://github.com/airbnb/caravel/pull/158) Refactor (@mistercrunch)
|
||||
- [#154](https://github.com/airbnb/caravel/pull/154) Digging into leap year bug and improvming tests (@mistercrunch)
|
||||
- [#157](https://github.com/airbnb/caravel/pull/157) add button to auto-copy short URLs in /explore page (@williaster)
|
||||
- [#149](https://github.com/airbnb/caravel/pull/149) Allowing to make certain widgets immune to filter (@mistercrunch)
|
||||
- [#151](https://github.com/airbnb/caravel/pull/151) Linting (@mistercrunch)
|
||||
- [#153](https://github.com/airbnb/caravel/pull/153) Improve README (@tay)
|
||||
- [#139](https://github.com/airbnb/caravel/pull/139) NPMification & Reactification (@williaster, @mistercrunch)
|
||||
- [#147](https://github.com/airbnb/caravel/pull/147) Tackling Featured Datasets (@mistercrunch)
|
||||
- [#148](https://github.com/airbnb/caravel/pull/148) Fix typo (@tay)
|
||||
- [#145](https://github.com/airbnb/caravel/pull/145) Moving files around ... yay! (@mistercrunch)
|
||||
- [#142](https://github.com/airbnb/caravel/pull/142) A few cosmetic fixes (nvd3 tooltips, buttons, tables) (@mistercrunch)
|
||||
- [#141](https://github.com/airbnb/caravel/pull/141) A simple base template for npm (@mistercrunch)
|
||||
- [#140](https://github.com/airbnb/caravel/pull/140) use the latest segment to extract metadata (@dayzzz)
|
||||
- [#136](https://github.com/airbnb/caravel/pull/136) Improved the bar char to allow for dimensional breakdowns (@mistercrunch)
|
||||
- [#134](https://github.com/airbnb/caravel/pull/134) Fixing the roles auto maintenance (@mistercrunch)
|
||||
- [#132](https://github.com/airbnb/caravel/pull/132) [nvd3] fixing the legend toggle bug (@mistercrunch)
|
||||
- [#131](https://github.com/airbnb/caravel/pull/131) More tests using doctests! (@mistercrunch)
|
||||
- [#130](https://github.com/airbnb/caravel/pull/130) Logging more (@mistercrunch)
|
||||
- [#129](https://github.com/airbnb/caravel/pull/129) Renaming Classes related to Druid (@mistercrunch)
|
||||
- [#127](https://github.com/airbnb/caravel/pull/127) SQL editor, eventually will be tied to a flow to create views (@mistercrunch)
|
||||
- [#128](https://github.com/airbnb/caravel/pull/128) Allowing definition of css templates (@mistercrunch)
|
||||
- [#126](https://github.com/airbnb/caravel/pull/126) New viz: Heatmap! (@mistercrunch)
|
||||
- [#125](https://github.com/airbnb/caravel/pull/125) Consistent colors rendered client side (@mistercrunch)
|
||||
- [#124](https://github.com/airbnb/caravel/pull/124) A more cohesive color strategy (@mistercrunch)
|
||||
|
||||
### 0.7.0 (2016/01/23 15:16 +00:00)
|
||||
- [#123](https://github.com/airbnb/caravel/pull/123) Adding a color factory (@mistercrunch)
|
||||
- [#122](https://github.com/airbnb/caravel/pull/122) Adding Parallel coordinates viz (@mistercrunch)
|
||||
- [#121](https://github.com/airbnb/caravel/pull/121) Iframe (@mistercrunch)
|
||||
- [#120](https://github.com/airbnb/caravel/pull/120) Slice information can be displayed in dashboard (@mistercrunch)
|
||||
- [#117](https://github.com/airbnb/caravel/pull/117) Doing some refactoring (@mistercrunch)
|
||||
- [#115](https://github.com/airbnb/caravel/pull/115) Providing options for Y axis number formating (@mistercrunch)
|
||||
- [#112](https://github.com/airbnb/caravel/pull/112) Adding an URL shortner (@mistercrunch)
|
||||
- [#113](https://github.com/airbnb/caravel/pull/113) Prettier checkboxes (@mistercrunch)
|
||||
- [#111](https://github.com/airbnb/caravel/pull/111) Loading another example amazing dash (@mistercrunch)
|
||||
- [#109](https://github.com/airbnb/caravel/pull/109) Getting browser history to work on the explore view (@mistercrunch)
|
||||
- [#108](https://github.com/airbnb/caravel/pull/108) pulling to the front on hover (@BradBaker)
|
||||
- [#104](https://github.com/airbnb/caravel/pull/104) simplifying tooltip code (@BradBaker)
|
||||
- [#105](https://github.com/airbnb/caravel/pull/105) adding stagger for all charts that have a date axis (@BradBaker)
|
||||
- [#102](https://github.com/airbnb/caravel/pull/102) Fix for 2-axis charts where it shrinks them a little bit (@bradmbaker, @BradBaker)
|
||||
- [#101](https://github.com/airbnb/caravel/pull/101) Add a Gitter chat badge to README.md (@gitter-badger)
|
||||
- [#100](https://github.com/airbnb/caravel/pull/100) Update tooltips with new classes (@bradmbaker)
|
||||
- [#99](https://github.com/airbnb/caravel/pull/99) Time resampling as in Pandas (@mistercrunch)
|
||||
- [#98](https://github.com/airbnb/caravel/pull/98) Change Scaling to Operate on SVG instead of Div (@bradmbaker)
|
||||
- [#96](https://github.com/airbnb/caravel/pull/96) Adding a filter box widget (@mistercrunch)
|
||||
- [#95](https://github.com/airbnb/caravel/pull/95) Working on docs (@mistercrunch)
|
||||
- [#94](https://github.com/airbnb/caravel/pull/94) Massive js refactor + Dashboard filters (@mistercrunch)
|
||||
- [#93](https://github.com/airbnb/caravel/pull/93) Controller (@mistercrunch)
|
||||
- [#92](https://github.com/airbnb/caravel/pull/92) Allowing not to group by on table view (@mistercrunch)
|
||||
- [#91](https://github.com/airbnb/caravel/pull/91) Exports (@mistercrunch)
|
||||
- [#90](https://github.com/airbnb/caravel/pull/90) A basic squeleton for the docs (@mistercrunch)
|
||||
- [#89](https://github.com/airbnb/caravel/pull/89) Featured datasets (@michellethomas)
|
||||
- [#87](https://github.com/airbnb/caravel/pull/87) fixing a few bugs with tool tip overflow (@BradBaker)
|
||||
- [#88](https://github.com/airbnb/caravel/pull/88) World Map viz with bubbles (@mistercrunch)
|
||||
- [#86](https://github.com/airbnb/caravel/pull/86) adjusting date formats (@BradBaker)
|
||||
- [#85](https://github.com/airbnb/caravel/pull/85) Adding sankey diagrams (@mistercrunch)
|
||||
- [#84](https://github.com/airbnb/caravel/pull/84) Adding directed force layout viz (@mistercrunch)
|
||||
- [#83](https://github.com/airbnb/caravel/pull/83) Big JS refactor (@mistercrunch)
|
||||
- [#82](https://github.com/airbnb/caravel/pull/82) letting tooltips in the dashboard overflow (@BradBaker)
|
||||
- [#81](https://github.com/airbnb/caravel/pull/81) Slightly better layout for explore page (@mistercrunch)
|
||||
- [#80](https://github.com/airbnb/caravel/pull/80) Checkboxes everywhere (@mistercrunch)
|
||||
- [#79](https://github.com/airbnb/caravel/pull/79) Cleanup around multiple select fields (@mistercrunch)
|
||||
|
||||
### 0.6.0 (2015/12/11 01:17 +00:00)
|
||||
- [#77](https://github.com/airbnb/caravel/pull/77) Better tooltips and more ways to integrate them easily (@mistercrunch)
|
||||
- [#76](https://github.com/airbnb/caravel/pull/76) Introducing form overrides for label and tooltips (@mistercrunch)
|
||||
- [#75](https://github.com/airbnb/caravel/pull/75) New viz: sunbursts (@mistercrunch)
|
||||
- [#74](https://github.com/airbnb/caravel/pull/74) Introducing fieldsets (@mistercrunch)
|
||||
- [#73](https://github.com/airbnb/caravel/pull/73) Airflowlike theme (@mistercrunch)
|
||||
- [#72](https://github.com/airbnb/caravel/pull/72) Logging slice and dash views (@mistercrunch)
|
||||
- [#70](https://github.com/airbnb/caravel/pull/70) Adding url slug support for dashboard model (@mistercrunch)
|
||||
- [#71](https://github.com/airbnb/caravel/pull/71) Add option to show minmax on x axis (@mistercrunch)
|
||||
- [#69](https://github.com/airbnb/caravel/pull/69) Allowing for [Save AS] and [Overwrite] (@mistercrunch)
|
||||
- [#68](https://github.com/airbnb/caravel/pull/68) Adding cumsum to rolling functions (@mistercrunch)
|
||||
- [#67](https://github.com/airbnb/caravel/pull/67) Fix debug mode calls get_json twice (@mistercrunch)
|
||||
- [#66](https://github.com/airbnb/caravel/pull/66) Adding a PivotTableViz (@mistercrunch)
|
||||
- [#65](https://github.com/airbnb/caravel/pull/65) Adding custom HAVING clause (@mistercrunch)
|
||||
- [#64](https://github.com/airbnb/caravel/pull/64) Preserving the ordering in selectmultiple (@mistercrunch)
|
||||
- [#63](https://github.com/airbnb/caravel/pull/63) Encrypting the passwords out of connection strings (@mistercrunch)
|
||||
- [#61](https://github.com/airbnb/caravel/pull/61) BetterBooleanField to fix html omitting non-checked <input> (@patrickleotardif)
|
||||
- [#60](https://github.com/airbnb/caravel/pull/60) Fix Markup Widget bug (@NiharikaRay)
|
||||
- [#59](https://github.com/airbnb/caravel/pull/59) Adding y-axis format option (@patrickleotardif)
|
||||
- [#58](https://github.com/airbnb/caravel/pull/58) Setting min_periods to 1 for rolling windows (@mistercrunch)
|
||||
- [#56](https://github.com/airbnb/caravel/pull/56) adding sort order of the slices on changed_on field (@mistercrunch)
|
||||
|
||||
### 0.5.2 (2015/10/24 01:06 +00:00)
|
||||
- [#53](https://github.com/airbnb/caravel/pull/53) Py3 (@mistercrunch)
|
||||
- [#51](https://github.com/airbnb/caravel/pull/51) Adding timezone offset as a datasource param (@mistercrunch)
|
||||
- [#52](https://github.com/airbnb/caravel/pull/52) Speed up travis builds with wheels (@mistercrunch)
|
||||
- [#48](https://github.com/airbnb/caravel/pull/48) Allowing to specify the gunicorn timeout in CLI and config (@mistercrunch)
|
||||
|
||||
### 0.5.0 (2015/10/13 01:09 +00:00)
|
||||
- [#46](https://github.com/airbnb/caravel/pull/46) Allowing to change the "Time Column" on SqlA (@mistercrunch)
|
||||
- [#45](https://github.com/airbnb/caravel/pull/45) Bootstrapping widgets from javascript initializer. (@akuhn)
|
||||
- [#43](https://github.com/airbnb/caravel/pull/43) Supporting arbitrary expressions (@mistercrunch)
|
||||
- [#42](https://github.com/airbnb/caravel/pull/42) Adding ability to style a dashboard with CSS (@mistercrunch)
|
||||
- [#41](https://github.com/airbnb/caravel/pull/41) Cleaning up the static folder (@mistercrunch)
|
||||
- [#35](https://github.com/airbnb/caravel/pull/35) A first draft on default security roles (@mistercrunch)
|
||||
- [#40](https://github.com/airbnb/caravel/pull/40) Introducing time comparison (@mistercrunch)
|
||||
- [#39](https://github.com/airbnb/caravel/pull/39) Adding interpolation choice for line charts (@mistercrunch)
|
||||
- [#38](https://github.com/airbnb/caravel/pull/38) Extract css rules and scripts into separate files. (@akuhn)
|
||||
- [#37](https://github.com/airbnb/caravel/pull/37) Viz type (@mistercrunch)
|
||||
- [#36](https://github.com/airbnb/caravel/pull/36) Extract widget javascript to separate files. (@akuhn)
|
||||
- [#34](https://github.com/airbnb/caravel/pull/34) Ripping out Highcharts. (@mistercrunch)
|
||||
|
||||
### 0.4.0 (2015/09/27 04:39 +00:00)
|
||||
- [#33](https://github.com/airbnb/caravel/pull/33) Adding nvd3 support (@mistercrunch)
|
||||
- [#32](https://github.com/airbnb/caravel/pull/32) Adding a foundation for unit tests (@mistercrunch)
|
||||
- [#31](https://github.com/airbnb/caravel/pull/31) Adding a button to test connections (@mistercrunch)
|
||||
- [#30](https://github.com/airbnb/caravel/pull/30) Word cloud widget! (@mistercrunch)
|
||||
- [#29](https://github.com/airbnb/caravel/pull/29) Adding support for markup (html/markdown) widgets (@mistercrunch)
|
||||
- [#28](https://github.com/airbnb/caravel/pull/28) Fix default Sqlite path. (@noddi)
|
||||
- [#27](https://github.com/airbnb/caravel/pull/27) More refactor and bugfixes (@mistercrunch)
|
||||
- [#26](https://github.com/airbnb/caravel/pull/26) Bugfix (@mistercrunch)
|
||||
- [#25](https://github.com/airbnb/caravel/pull/25) Adding basic dashboarding support! (@mistercrunch)
|
||||
- [#23](https://github.com/airbnb/caravel/pull/23) Custom WHERE clause for tables (not druid) + error handling refactor (@mistercrunch)
|
||||
- [#22](https://github.com/airbnb/caravel/pull/22) Form factory refactor (@mistercrunch)
|
||||
- [#20](https://github.com/airbnb/caravel/pull/20) add tzinfo config, useful when start druid without utc timezone (@wbchn)
|
||||
|
||||
### 0.2.1 (2015/09/05 22:08 +00:00)
|
||||
- [#19](https://github.com/airbnb/caravel/pull/19) Preparing pypi package (@mistercrunch)
|
||||
|
||||
### 0.2.0 (2015/09/05 20:43 +00:00)
|
||||
- [#16](https://github.com/airbnb/caravel/pull/16) Adding Bubble charts (@mistercrunch)
|
||||
- [#13](https://github.com/airbnb/caravel/pull/13) Now supporting SQL Multiple database (@mistercrunch)
|
||||
- [#12](https://github.com/airbnb/caravel/pull/12) Cosmetricks (@mistercrunch)
|
||||
- [#11](https://github.com/airbnb/caravel/pull/11) Fixing the ways metrics are autogenerated (@mistercrunch)
|
||||
- [#10](https://github.com/airbnb/caravel/pull/10) Now enabling multi-cluster, connection info managed in UI (@mistercrunch)
|
||||
- [#9](https://github.com/airbnb/caravel/pull/9) Multi delete action on datasources (@mistercrunch)
|
||||
- [#8](https://github.com/airbnb/caravel/pull/8) Preventing bad json from creating problems (@mistercrunch)
|
||||
- [#3](https://github.com/airbnb/caravel/pull/3) Implementing my own highcharts wrapper (@mistercrunch)
|
||||
@@ -4,7 +4,7 @@ to add your organization and/or project to this document!
|
||||
Organizations
|
||||
----------
|
||||
- [Airbnb](https://github.com/airbnb)
|
||||
- your company name here!
|
||||
- [GfK Data Lab] (http://datalab.gfk.com)
|
||||
|
||||
Projects
|
||||
----------
|
||||
|
||||
16
README.md
@@ -1,6 +1,6 @@
|
||||
Caravel
|
||||
=========
|
||||
<img src="http://i.imgur.com/H0Kyvyi.jpg" alt="Caravel" width="500"/>
|
||||
<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)
|
||||
@@ -31,7 +31,8 @@ Caravel'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:
|
||||
* A quick way to intuitively visualize datasets by allowing users to create and share interactive dashboards
|
||||
* 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
|
||||
way to extend the capabilities
|
||||
* An extensible, high granularity security model allowing intricate rules
|
||||
@@ -39,9 +40,9 @@ Caravel provides:
|
||||
authentication providers (database, OpenID, LDAP, OAuth & REMOTE_USER
|
||||
through Flask AppBuiler)
|
||||
* A simple semantic layer, allowing to control how data sources are
|
||||
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
|
||||
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
|
||||
slicing and dicing large, realtime datasets
|
||||
|
||||
@@ -50,7 +51,7 @@ Database Support
|
||||
----------------
|
||||
|
||||
Caravel was originally designed on top of Druid.io, but quickly broadened
|
||||
its scope to support other databases through the use of SqlAlchemy, a Python
|
||||
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).
|
||||
|
||||
@@ -84,10 +85,11 @@ More screenshots
|
||||

|
||||
|
||||
|
||||
Related Links
|
||||
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)
|
||||
|
||||
|
||||
Tip of the Hat
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""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
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
error = (
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
"MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\n"+
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
"newline-after-var": [0],
|
||||
"no-bitwise": [0],
|
||||
"no-cond-assign": [2],
|
||||
"no-console": [2],
|
||||
"no-console": [1, { allow: ["warn", "error"] }],
|
||||
"no-const-assign": [2],
|
||||
"no-constant-condition": [2],
|
||||
"no-control-regex": [2],
|
||||
|
||||
BIN
caravel/assets/images/caravel.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
caravel/assets/images/caravel_logo.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
caravel/assets/images/tutorial/add_db.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
caravel/assets/images/tutorial/created.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
caravel/assets/images/tutorial/db_added.png
Normal file
|
After Width: | Height: | Size: 245 KiB |
BIN
caravel/assets/images/tutorial/db_menu.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
caravel/assets/images/tutorial/db_plus.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
caravel/assets/images/tutorial/explore.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
caravel/assets/images/tutorial/in_new_dash.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
caravel/assets/images/tutorial/matrix.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
caravel/assets/images/tutorial/new_dash.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
caravel/assets/images/tutorial/pen.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
caravel/assets/images/tutorial/search.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
caravel/assets/images/viz_thumbnails/bar.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
caravel/assets/images/viz_thumbnails/big_number.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
caravel/assets/images/viz_thumbnails/bubble.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
caravel/assets/images/viz_thumbnails/filter.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
caravel/assets/images/viz_thumbnails/force_directed.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
caravel/assets/images/viz_thumbnails/line.png
Normal file
|
After Width: | Height: | Size: 314 KiB |
BIN
caravel/assets/images/viz_thumbnails/percent_change.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
BIN
caravel/assets/images/viz_thumbnails/pie.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
caravel/assets/images/viz_thumbnails/pivot_table.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
caravel/assets/images/viz_thumbnails/sankey.png
Normal file
|
After Width: | Height: | Size: 200 KiB |
BIN
caravel/assets/images/viz_thumbnails/stacked.png
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
caravel/assets/images/viz_thumbnails/sunburst.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
caravel/assets/images/viz_thumbnails/table.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
caravel/assets/images/viz_thumbnails/word_cloud.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
@@ -2,6 +2,7 @@ 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');
|
||||
@@ -21,12 +22,17 @@ var Dashboard = function (dashboardData) {
|
||||
var sliceObjects = [],
|
||||
dash = this;
|
||||
dashboard.slices.forEach(function (data) {
|
||||
var slice = px.Slice(data, dash);
|
||||
$("#slice_" + data.slice_id).find('a.refresh').click(function () {
|
||||
slice.render(true);
|
||||
});
|
||||
sliceObjects.push(slice);
|
||||
slice.render();
|
||||
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;
|
||||
},
|
||||
@@ -142,10 +148,17 @@ var Dashboard = function (dashboardData) {
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
success: function () {
|
||||
alert("Saved!");
|
||||
showModal({
|
||||
title: "Success",
|
||||
body: "This dashboard was saved successfully."
|
||||
});
|
||||
},
|
||||
error: function () {
|
||||
alert("Error :(");
|
||||
error: function (error) {
|
||||
showModal({
|
||||
title: "Error",
|
||||
body: "Sorry, there was an error saving this dashboard:<br />" + error
|
||||
});
|
||||
console.warn("Save dashboard error", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -173,7 +186,10 @@ var Dashboard = function (dashboardData) {
|
||||
|
||||
});
|
||||
$('#filters').click(function () {
|
||||
alert(dashboard.readFilters());
|
||||
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) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
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
|
||||
@@ -53,7 +54,7 @@ function prepForm() {
|
||||
});
|
||||
}
|
||||
|
||||
function druidify(force, pushState) {
|
||||
function query(force, pushState) {
|
||||
if (force === undefined) {
|
||||
force = false;
|
||||
}
|
||||
@@ -89,9 +90,9 @@ function initExploreView() {
|
||||
|
||||
if (parent.hasClass("collapsed")) {
|
||||
if (animation) {
|
||||
parent.find(".fieldset_content").slideDown();
|
||||
parent.find(".panel-body").slideDown();
|
||||
} else {
|
||||
parent.find(".fieldset_content").show();
|
||||
parent.find(".panel-body").show();
|
||||
}
|
||||
parent.removeClass("collapsed");
|
||||
parent.find("span.collapser").text("[-]");
|
||||
@@ -103,9 +104,9 @@ function initExploreView() {
|
||||
}
|
||||
} else { // not collapsed
|
||||
if (animation) {
|
||||
parent.find(".fieldset_content").slideUp();
|
||||
parent.find(".panel-body").slideUp();
|
||||
} else {
|
||||
parent.find(".fieldset_content").hide();
|
||||
parent.find(".panel-body").hide();
|
||||
}
|
||||
|
||||
parent.addClass("collapsed");
|
||||
@@ -121,8 +122,9 @@ function initExploreView() {
|
||||
|
||||
px.initFavStars();
|
||||
|
||||
$('legend').click(function () {
|
||||
$('form .panel-heading').click(function () {
|
||||
toggle_fieldset($(this), true);
|
||||
$(this).css('cursor', 'pointer');
|
||||
});
|
||||
|
||||
function copyURLToClipboard(url) {
|
||||
@@ -181,15 +183,25 @@ function initExploreView() {
|
||||
$shortner.popover('destroy');
|
||||
}
|
||||
},
|
||||
error: function () {
|
||||
alert("Error :(");
|
||||
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);
|
||||
@@ -260,8 +272,8 @@ function initExploreView() {
|
||||
}
|
||||
});
|
||||
|
||||
$(".druidify").click(function () {
|
||||
druidify(true);
|
||||
$(".query").click(function () {
|
||||
query(true);
|
||||
});
|
||||
|
||||
function create_choices(term, data) {
|
||||
@@ -325,7 +337,7 @@ $(document).ready(function () {
|
||||
$('.slice').data('slice', slice);
|
||||
|
||||
// call vis render method, which issues ajax
|
||||
druidify(false, false);
|
||||
query(false, false);
|
||||
|
||||
// make checkbox inputs display as toggles
|
||||
$(':checkbox')
|
||||
|
||||
@@ -2,14 +2,13 @@ var $ = require('jquery');
|
||||
var jQuery = $;
|
||||
var d3 = require('d3');
|
||||
|
||||
require('../../stylesheets/caravel.css');
|
||||
|
||||
// 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',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
var $ = require('jquery');
|
||||
var d3 = require('d3');
|
||||
|
||||
/*
|
||||
@@ -50,6 +51,30 @@ function wrapSvgText(text, width, 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
|
||||
wrapSvgText: wrapSvgText,
|
||||
showModal: showModal
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
var showModal = require('./modules/utils.js').showModal;
|
||||
|
||||
require('select2');
|
||||
require('datatables');
|
||||
require('datatables.net-bs');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
require('bootstrap');
|
||||
|
||||
var ace = require('brace');
|
||||
@@ -41,7 +44,10 @@ $(document).ready(function () {
|
||||
$("#dbtable").on("change", showTableMetadata);
|
||||
showTableMetadata();
|
||||
$("#create_view").click(function () {
|
||||
alert("Not implemented");
|
||||
showModal({
|
||||
title: "Error",
|
||||
body: "Sorry, this feature is not yet implemented"
|
||||
});
|
||||
});
|
||||
$(".sqlcontent").show();
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
|
||||
require('../stylesheets/caravel.css');
|
||||
require('../stylesheets/welcome.css');
|
||||
require('bootstrap');
|
||||
require('datatables');
|
||||
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, modelEndpoint, ordering) {
|
||||
function modelViewTable(selector, modelEndpoint) {
|
||||
// Builds a dataTable from a flask appbuilder api endpoint
|
||||
$.getJSON(modelEndpoint + '/api/read', function (data) {
|
||||
var tableData = jQuery.map(data.result, function (el, i) {
|
||||
@@ -21,13 +21,32 @@ function modelViewTable(selector, modelEndpoint, ordering) {
|
||||
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: false,
|
||||
order: ordering,
|
||||
searching: false
|
||||
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' });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"d3-tip": "^0.6.7",
|
||||
"datamaps": "^0.4.4",
|
||||
"datatables-bootstrap3-plugin": "^0.4.0",
|
||||
"datatables.net": "^1.10.11",
|
||||
"datatables.net-bs": "^1.10.11",
|
||||
"exports-loader": "^0.6.3",
|
||||
"font-awesome": "^4.5.0",
|
||||
"gridster": "^0.5.6",
|
||||
|
||||
@@ -116,25 +116,6 @@ span.title-block {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
fieldset.fs-style {
|
||||
font-family: Verdana, Arial, sans-serif;
|
||||
font-size: small;
|
||||
font-weight: normal;
|
||||
border: 1px solid #CCC;
|
||||
background-color: #F4F4F4;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
margin: 0px 0px 10px 0px;
|
||||
}
|
||||
legend.legend-style {
|
||||
font-size: 14px;
|
||||
padding: 0px 6px;
|
||||
cursor: pointer;
|
||||
margin: 0px;
|
||||
color: #444;
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
.nvtooltip {
|
||||
//position: relative !important;
|
||||
z-index: 888;
|
||||
@@ -142,10 +123,6 @@ legend.legend-style {
|
||||
.nvtooltip table td{
|
||||
font-size: 11px !important;
|
||||
}
|
||||
legend {
|
||||
width: auto;
|
||||
border-bottom: 0px;
|
||||
}
|
||||
.navbar {
|
||||
-webkit-box-shadow: 0px 3px 3px #AAA;
|
||||
-moz-box-shadow: 0px 3px 3px #AAA;
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
.welcome .widget{
|
||||
border-radius: 0;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 2px 1px 5px -2px #aaa;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.welcome .widget .header {
|
||||
background-color: #f1f1f1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.welcome .widget>div {
|
||||
padding: 3px;
|
||||
overflow: auto;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.table i {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
img.loading {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.welcome table {
|
||||
display: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
margin-left: 5px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.big_number g.axis text {
|
||||
.big_number g.axis text,
|
||||
.big_number_total g.axis text {
|
||||
font-size: 10px;
|
||||
font-weight: normal;
|
||||
color: gray;
|
||||
@@ -8,18 +9,21 @@
|
||||
font-weight: none;
|
||||
}
|
||||
|
||||
.big_number text.big {
|
||||
.big_number text.big,
|
||||
.big_number_total text.big{
|
||||
stroke: black;
|
||||
text-anchor: middle;
|
||||
fill: black;
|
||||
}
|
||||
|
||||
.big_number g.tick line {
|
||||
.big_number g.tick line,
|
||||
.big_number_total g.tick line{
|
||||
stroke-width: 1px;
|
||||
stroke: grey;
|
||||
}
|
||||
|
||||
.big_number .domain {
|
||||
.big_number .domain,
|
||||
.big_number_total .domain{
|
||||
fill: none;
|
||||
stroke: black;
|
||||
stroke-width: 1;
|
||||
|
||||
@@ -31,7 +31,12 @@ function bigNumberVis(slice) {
|
||||
var data = json.data;
|
||||
var compare_suffix = ' ' + json.compare_suffix;
|
||||
var v_compare = null;
|
||||
var v = data[data.length - 1][1];
|
||||
var v = null;
|
||||
if (data.length > 1) {
|
||||
v = data[data.length - 1][1];
|
||||
} else {
|
||||
v = data[data.length - 1][0];
|
||||
}
|
||||
if (json.compare_lag > 0) {
|
||||
var pos = data.length - (json.compare_lag + 1);
|
||||
if (pos >= 0) {
|
||||
@@ -97,6 +102,19 @@ function bigNumberVis(slice) {
|
||||
.style('font-size', d3.min([height, width]) / 3.5)
|
||||
.attr('fill', 'white');
|
||||
|
||||
//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 %
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
var $ = window.$ = require('jquery');
|
||||
var jQuery = window.jQuery = $;
|
||||
|
||||
require('datatables');
|
||||
require('datatables.net-bs');
|
||||
require('./pivot_table.css');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ function sankeyVis(slice) {
|
||||
|
||||
var formatNumber = d3.format(",.0f");
|
||||
|
||||
div.selectAll("*").remove();
|
||||
var svg = div.append("svg")
|
||||
.attr("width", width + margin.left + margin.right)
|
||||
.attr("height", height + margin.top + margin.bottom)
|
||||
|
||||
@@ -188,7 +188,7 @@ function sunburstVis(slice) {
|
||||
arcs.selectAll("path")
|
||||
.style("stroke-width", null)
|
||||
.style("stroke", null)
|
||||
.style("opacity", 0.3);
|
||||
.style("opacity", 0.7);
|
||||
|
||||
// Then highlight only those that are an ancestor of the current segment.
|
||||
arcs.selectAll("path")
|
||||
|
||||
@@ -3,7 +3,7 @@ var jQuery = window.jQuery = $;
|
||||
var d3 = require('d3');
|
||||
|
||||
require('./table.css');
|
||||
require('datatables');
|
||||
require('datatables.net-bs');
|
||||
require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
|
||||
|
||||
function tableVis(slice) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
@@ -4,6 +4,11 @@ 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 flask_appbuilder.security.manager import AUTH_DB
|
||||
from dateutil import tz
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""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
|
||||
@@ -529,6 +533,16 @@ def load_birth_names():
|
||||
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',
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""This module contains data related to countries and is used for geo mapping"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
countries = [
|
||||
{
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""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 wtforms import (
|
||||
Form, SelectMultipleField, SelectField, TextField, TextAreaField,
|
||||
@@ -500,6 +504,11 @@ class FormFactory(object):
|
||||
"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
|
||||
|
||||
@@ -28,10 +28,8 @@ def upgrade():
|
||||
sa.Column('broker_port', sa.Integer(), nullable=True),
|
||||
sa.Column('broker_endpoint', sa.String(length=256), nullable=True),
|
||||
sa.Column('metadata_last_refreshed', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('cluster_name')
|
||||
)
|
||||
@@ -41,10 +39,8 @@ def upgrade():
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('dashboard_title', sa.String(length=500), nullable=True),
|
||||
sa.Column('position_json', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('dbs',
|
||||
@@ -53,10 +49,8 @@ def upgrade():
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('database_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('sqlalchemy_uri', sa.String(length=1024), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('database_name')
|
||||
)
|
||||
@@ -69,14 +63,10 @@ def upgrade():
|
||||
sa.Column('is_hidden', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('default_endpoint', sa.Text(), nullable=True),
|
||||
sa.Column('user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('cluster_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=False),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['cluster_name'], ['clusters.cluster_name'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
|
||||
sa.Column('user_id', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('cluster_name', sa.String(length=250), sa.ForeignKey("clusters.cluster_name"), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('datasource_name')
|
||||
)
|
||||
@@ -87,12 +77,9 @@ def upgrade():
|
||||
sa.Column('table_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('main_dttm_col', sa.String(length=250), nullable=True),
|
||||
sa.Column('default_endpoint', sa.Text(), nullable=True),
|
||||
sa.Column('database_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['database_id'], ['dbs.id'], ),
|
||||
sa.Column('database_id', sa.Integer(), sa.ForeignKey("dbs.id"), nullable=False),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('table_name')
|
||||
)
|
||||
@@ -101,7 +88,7 @@ def upgrade():
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('datasource_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('column_name', sa.String(length=256), nullable=True),
|
||||
sa.Column('column_name', sa.String(length=256), sa.ForeignKey("datasources.datasource_name"), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
sa.Column('type', sa.String(length=32), nullable=True),
|
||||
sa.Column('groupby', sa.Boolean(), nullable=True),
|
||||
@@ -111,11 +98,8 @@ def upgrade():
|
||||
sa.Column('min', sa.Boolean(), nullable=True),
|
||||
sa.Column('filterable', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('metrics',
|
||||
@@ -123,7 +107,7 @@ def upgrade():
|
||||
sa.Column('metric_name', sa.String(length=512), nullable=True),
|
||||
sa.Column('verbose_name', sa.String(length=1024), nullable=True),
|
||||
sa.Column('metric_type', sa.String(length=32), nullable=True),
|
||||
sa.Column('datasource_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('datasource_name', sa.String(length=250), sa.ForeignKey("datasources.datasource_name"), nullable=True),
|
||||
sa.Column('json', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
|
||||
@@ -134,18 +118,14 @@ def upgrade():
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('slice_name', sa.String(length=250), nullable=True),
|
||||
sa.Column('druid_datasource_id', sa.Integer(), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), nullable=True),
|
||||
sa.Column('druid_datasource_id', sa.Integer(), sa.ForeignKey("datasources.id"), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), sa.ForeignKey("tables.id"), nullable=True),
|
||||
sa.Column('datasource_type', sa.String(length=200), nullable=True),
|
||||
sa.Column('datasource_name', sa.String(length=2000), nullable=True),
|
||||
sa.Column('viz_type', sa.String(length=250), nullable=True),
|
||||
sa.Column('params', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['druid_datasource_id'], ['datasources.id'], ),
|
||||
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('sql_metrics',
|
||||
@@ -155,21 +135,18 @@ def upgrade():
|
||||
sa.Column('metric_name', sa.String(length=512), nullable=True),
|
||||
sa.Column('verbose_name', sa.String(length=1024), nullable=True),
|
||||
sa.Column('metric_type', sa.String(length=32), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), sa.ForeignKey("tables.id"), nullable=True),
|
||||
sa.Column('expression', sa.Text(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('table_columns',
|
||||
sa.Column('created_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('changed_on', sa.DateTime(), nullable=False),
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('table_id', sa.Integer(), nullable=True),
|
||||
sa.Column('table_id', sa.Integer(), sa.ForeignKey("tables.id"), nullable=True),
|
||||
sa.Column('column_name', sa.String(length=256), nullable=True),
|
||||
sa.Column('is_dttm', sa.Boolean(), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||
@@ -181,19 +158,14 @@ def upgrade():
|
||||
sa.Column('min', sa.Boolean(), nullable=True),
|
||||
sa.Column('filterable', sa.Boolean(), nullable=True),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('created_by_fk', sa.Integer(), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
|
||||
sa.ForeignKeyConstraint(['table_id'], ['tables.id'], ),
|
||||
sa.Column('created_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.Column('changed_by_fk', sa.Integer(), sa.ForeignKey("ab_user.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_table('dashboard_slices',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('dashboard_id', sa.Integer(), nullable=True),
|
||||
sa.Column('slice_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['dashboard_id'], ['dashboards.id'], ),
|
||||
sa.ForeignKeyConstraint(['slice_id'], ['slices.id'], ),
|
||||
sa.Column('dashboard_id', sa.Integer(), sa.ForeignKey("dashboards.id"), nullable=True),
|
||||
sa.Column('slice_id', sa.Integer(), sa.ForeignKey("slices.id"), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
### end Alembic commands ###
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""Adding extra field to Database model
|
||||
|
||||
Revision ID: 867bf4f117f9
|
||||
Revises: fee7b758c130
|
||||
Create Date: 2016-04-03 15:23:20.280841
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '867bf4f117f9'
|
||||
down_revision = 'fee7b758c130'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('dbs', sa.Column('extra', sa.Text(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('dbs', 'extra')
|
||||
@@ -1,4 +1,8 @@
|
||||
"""A collection of ORM sqlalchemy models for Caravel"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from copy import deepcopy, copy
|
||||
from collections import namedtuple
|
||||
@@ -9,6 +13,7 @@ import logging
|
||||
from six import string_types
|
||||
import sqlparse
|
||||
import requests
|
||||
import textwrap
|
||||
|
||||
from dateutil.parser import parse
|
||||
from flask import flash, request, g
|
||||
@@ -151,10 +156,8 @@ class Slice(Model, AuditMixinNullable):
|
||||
@utils.memoized
|
||||
def viz(self):
|
||||
d = json.loads(self.params)
|
||||
viz = viz_types[self.viz_type](
|
||||
self.datasource,
|
||||
form_data=d)
|
||||
return viz
|
||||
viz_class = viz_types[self.viz_type]
|
||||
return viz_class(self.datasource, form_data=d)
|
||||
|
||||
@property
|
||||
def description_markeddown(self):
|
||||
@@ -166,7 +169,13 @@ class Slice(Model, AuditMixinNullable):
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
d = self.viz.data
|
||||
d = {}
|
||||
self.token = ''
|
||||
try:
|
||||
d = self.viz.data
|
||||
self.token = d.get('token')
|
||||
except Exception as e:
|
||||
d['error'] = str(e)
|
||||
d['slice_id'] = self.id
|
||||
return d
|
||||
|
||||
@@ -186,8 +195,8 @@ class Slice(Model, AuditMixinNullable):
|
||||
slice_params['slice_name'] = self.slice_name
|
||||
from werkzeug.urls import Href
|
||||
href = Href(
|
||||
"/caravel/explore/{self.datasource_type}/"
|
||||
"{self.datasource_id}/".format(self=self))
|
||||
"/caravel/explore/{obj.datasource_type}/"
|
||||
"{obj.datasource_id}/".format(obj=self))
|
||||
return href(slice_params)
|
||||
|
||||
@property
|
||||
@@ -197,8 +206,8 @@ class Slice(Model, AuditMixinNullable):
|
||||
@property
|
||||
def slice_link(self):
|
||||
url = self.slice_url
|
||||
return '<a href="{url}">{self.slice_name}</a>'.format(
|
||||
url=url, self=self)
|
||||
return '<a href="{url}">{obj.slice_name}</a>'.format(
|
||||
url=url, obj=self)
|
||||
|
||||
|
||||
dashboard_slices = Table(
|
||||
@@ -239,7 +248,7 @@ class Dashboard(Model, AuditMixinNullable):
|
||||
return {}
|
||||
|
||||
def dashboard_link(self):
|
||||
return '<a href="{self.url}">{self.dashboard_title}</a>'.format(self=self)
|
||||
return '<a href="{obj.url}">{obj.dashboard_title}</a>'.format(obj=self)
|
||||
|
||||
@property
|
||||
def json_data(self):
|
||||
@@ -288,12 +297,20 @@ class Database(Model, AuditMixinNullable):
|
||||
sqlalchemy_uri = Column(String(1024))
|
||||
password = Column(EncryptedType(String(1024), config.get('SECRET_KEY')))
|
||||
cache_timeout = Column(Integer)
|
||||
extra = Column(Text, default=textwrap.dedent("""\
|
||||
{
|
||||
"metadata_params": {},
|
||||
"engine_params": {}
|
||||
}
|
||||
"""))
|
||||
|
||||
def __repr__(self):
|
||||
return self.database_name
|
||||
|
||||
def get_sqla_engine(self):
|
||||
return create_engine(self.sqlalchemy_uri_decrypted)
|
||||
extra = self.get_extra()
|
||||
params = extra.get('engine_params', {})
|
||||
return create_engine(self.sqlalchemy_uri_decrypted, **params)
|
||||
|
||||
def safe_sqlalchemy_uri(self):
|
||||
return self.sqlalchemy_uri
|
||||
@@ -324,7 +341,16 @@ class Database(Model, AuditMixinNullable):
|
||||
Grain('week', 'DATE_SUB({col}, INTERVAL DAYOFWEEK({col}) - 1 DAY)'),
|
||||
Grain('month', 'DATE_SUB({col}, INTERVAL DAYOFMONTH({col}) - 1 DAY)'),
|
||||
),
|
||||
'postgresql': (
|
||||
Grain("Time Column", "{col}"),
|
||||
Grain("hour", "DATE_TRUNC('hour', {col})"),
|
||||
Grain("day", "DATE_TRUNC('day', {col})"),
|
||||
Grain("week", "DATE_TRUNC('week', {col})"),
|
||||
Grain("month", "DATE_TRUNC('month', {col})"),
|
||||
Grain("year", "DATE_TRUNC('year', {col})"),
|
||||
),
|
||||
}
|
||||
db_time_grains['redshift'] = db_time_grains['postgresql']
|
||||
for db_type, grains in db_time_grains.items():
|
||||
if self.sqlalchemy_uri.startswith(db_type):
|
||||
return grains
|
||||
@@ -332,8 +358,18 @@ class Database(Model, AuditMixinNullable):
|
||||
def grains_dict(self):
|
||||
return {grain.name: grain for grain in self.grains()}
|
||||
|
||||
def get_extra(self):
|
||||
extra = {}
|
||||
if self.extra:
|
||||
try:
|
||||
extra = json.loads(self.extra)
|
||||
except Exception as e:
|
||||
logging.error(e)
|
||||
return extra
|
||||
|
||||
def get_table(self, table_name):
|
||||
meta = MetaData()
|
||||
extra = self.get_extra()
|
||||
meta = MetaData(**extra.get('metadata_params', {}))
|
||||
return Table(
|
||||
table_name, meta,
|
||||
autoload=True,
|
||||
@@ -400,12 +436,12 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
|
||||
@property
|
||||
def perm(self):
|
||||
return (
|
||||
"[{self.database}].[{self.table_name}]"
|
||||
"(id:{self.id})").format(self=self)
|
||||
"[{obj.database}].[{obj.table_name}]"
|
||||
"(id:{obj.id})").format(obj=self)
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return "[{self.database}].[{self.table_name}]".format(self=self)
|
||||
return "[{obj.database}].[{obj.table_name}]".format(obj=self)
|
||||
|
||||
@property
|
||||
def dttm_cols(self):
|
||||
@@ -435,11 +471,16 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
|
||||
def name(self):
|
||||
return self.table_name
|
||||
|
||||
@property
|
||||
def explore_url(self):
|
||||
if self.default_endpoint:
|
||||
return self.default_endpoint
|
||||
else:
|
||||
return "/caravel/explore/{obj.type}/{obj.id}/".format(obj=self)
|
||||
|
||||
@property
|
||||
def table_link(self):
|
||||
url = "/caravel/explore/{self.type}/{self.id}/".format(self=self)
|
||||
return '<a href="{url}">{self.table_name}</a>'.format(
|
||||
url=url, self=self)
|
||||
return '<a href="{obj.explore_url}">{obj.table_name}</a>'.format(obj=self)
|
||||
|
||||
@property
|
||||
def metrics_combo(self):
|
||||
@@ -571,6 +612,8 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
|
||||
having_clause_and += [text(extras['having'])]
|
||||
if granularity:
|
||||
qry = qry.where(and_(*(time_filter + where_clause_and)))
|
||||
else:
|
||||
qry = qry.where(and_(*where_clause_and))
|
||||
qry = qry.having(and_(*having_clause_and))
|
||||
if groupby:
|
||||
qry = qry.order_by(desc(main_metric_expr))
|
||||
@@ -605,7 +648,6 @@ class SqlaTable(Model, Queryable, AuditMixinNullable):
|
||||
|
||||
def fetch_metadata(self):
|
||||
"""Fetches the metadata for the table and merges it in"""
|
||||
table = self.database.get_table(self.table_name)
|
||||
try:
|
||||
table = self.database.get_table(self.table_name)
|
||||
except Exception as e:
|
||||
@@ -778,9 +820,9 @@ class DruidCluster(Model, AuditMixinNullable):
|
||||
|
||||
def refresh_datasources(self):
|
||||
endpoint = (
|
||||
"http://{self.coordinator_host}:{self.coordinator_port}/"
|
||||
"{self.coordinator_endpoint}/datasources"
|
||||
).format(self=self)
|
||||
"http://{obj.coordinator_host}:{obj.coordinator_port}/"
|
||||
"{obj.coordinator_endpoint}/datasources"
|
||||
).format(obj=self)
|
||||
|
||||
datasources = json.loads(requests.get(endpoint).text)
|
||||
for datasource in datasources:
|
||||
@@ -824,8 +866,8 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
|
||||
@property
|
||||
def perm(self):
|
||||
return (
|
||||
"[{self.cluster_name}].[{self.datasource_name}]"
|
||||
"(id:{self.id})").format(self=self)
|
||||
"[{obj.cluster_name}].[{obj.datasource_name}]"
|
||||
"(id:{obj.id})").format(obj=self)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
@@ -840,17 +882,17 @@ class DruidDatasource(Model, AuditMixinNullable, Queryable):
|
||||
@property
|
||||
def full_name(self):
|
||||
return (
|
||||
"[{self.cluster_name}]."
|
||||
"[{self.datasource_name}]").format(self=self)
|
||||
"[{obj.cluster_name}]."
|
||||
"[{obj.datasource_name}]").format(obj=self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.datasource_name
|
||||
|
||||
@property
|
||||
def datasource_link(self):
|
||||
url = "/caravel/explore/{self.type}/{self.id}/".format(self=self)
|
||||
return '<a href="{url}">{self.datasource_name}</a>'.format(
|
||||
url=url, self=self)
|
||||
url = "/caravel/explore/{obj.type}/{obj.id}/".format(obj=self)
|
||||
return '<a href="{url}">{obj.datasource_name}</a>'.format(
|
||||
url=url, obj=self)
|
||||
|
||||
def get_metric_obj(self, metric_name):
|
||||
return [
|
||||
|
||||
@@ -12,15 +12,13 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
{% if appbuilder.app_icon %}
|
||||
<a class="navbar-brand" style="padding:10px;" href="{{appbuilder.get_url_for_index}}">
|
||||
<img width="30" src="{{appbuilder.app_icon}}">
|
||||
<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>
|
||||
{% endif %}
|
||||
<span class="navbar-brand">
|
||||
<a href="{{appbuilder.get_url_for_index}}">
|
||||
{{ appbuilder.app_name }}
|
||||
</a>
|
||||
<a href="{{appbuilder.get_url_for_index}}">
|
||||
{{ appbuilder.app_name }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
|
||||
4
caravel/templates/caravel/add_slice.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<script>
|
||||
var msg = "Click on a table link to create a Slice";
|
||||
window.location = "/r/msg/?url={{ '/tablemodelview/list/' }}&msg=" + msg;
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "appbuilder/baselayout.html" %}
|
||||
|
||||
{% block head_css %}
|
||||
<link rel="icon" type="image/png" href="/static/img/favicon.png">
|
||||
<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 %}
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
{% block head_meta %}{% endblock %}
|
||||
{% block head_css %}
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/node_modules/font-awesome/css/font-awesome.min.css" />
|
||||
<link rel="icon" type="image/png" href="/static/img/favicon.png">
|
||||
<link rel="stylesheet" type="text/css" href="/static/assets/stylesheets/caravel.css" />
|
||||
<link rel="icon" type="image/png" href="/static/assets/images/favicon.png">
|
||||
{% endblock %}
|
||||
{% block head_js %}
|
||||
<script src="/static/assets/javascripts/dist/css-theme.entry.js"></script>
|
||||
@@ -30,6 +31,24 @@
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<!-- Modal for misc messages / alerts -->
|
||||
<div class="misc-modal modal fade" 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"></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% block tail_js %}
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -68,21 +68,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gridster content_fluid" style="visibility: hidden;">
|
||||
<ul>
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
||||
{% set viz = slice.viz %}
|
||||
|
||||
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
slice_id="{{ slice.id }}"
|
||||
class="widget {{ slice.viz.viz_type }}"
|
||||
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">
|
||||
@@ -124,9 +125,9 @@
|
||||
</div>
|
||||
<div class="row chart-container">
|
||||
<input type="hidden" slice_id="{{ slice.id }}" value="false">
|
||||
<div id="{{ viz.token }}" class="token col-md-12">
|
||||
<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="{{ viz.token }}_con"></div>
|
||||
<div class="slice_container" id="{{ slice.token }}_con"></div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -29,18 +29,16 @@
|
||||
<div class="datasource container-fluid">
|
||||
<form id="query" method="GET" style="display: none;">
|
||||
<div class="header">
|
||||
<span class="btn btn-default notbtn" title="datasource" data-toggle="tooltip">
|
||||
{{ datasource.full_name }}
|
||||
{% if datasource.description %}
|
||||
<a data-toggle="modal" data-target="#sourceinfo_modal">
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip" data-placement="bottom" title="{{ datasource.description }}"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a class="" href="/{{ datasource.baselink }}/edit/{{ datasource.id }}" data-toggle="tooltip" title="Edit">
|
||||
<i class="fa fa-edit"></i>
|
||||
</a>
|
||||
<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>
|
||||
<span title="Visualization Type" data-toggle="tooltip">
|
||||
{{ form.get_field("viz_type")(class_="select2") }}
|
||||
</span>
|
||||
<span>{{ form.get_field("viz_type")(class_="select2") }}</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>
|
||||
@@ -81,7 +79,7 @@
|
||||
<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 druidify">
|
||||
<button type="button" class="btn btn-primary query">
|
||||
<i class="fa fa-bolt"></i>Query
|
||||
</button>
|
||||
{% if viz.form_data.slice_id %}
|
||||
@@ -96,9 +94,9 @@
|
||||
</div>
|
||||
<br/>
|
||||
{% for fieldset in form.fieldsets %}
|
||||
<fieldset class="fs-style">
|
||||
<div class="panel panel-default">
|
||||
{% if fieldset.label %}
|
||||
<legend class="legend-style">
|
||||
<div class="panel-heading">
|
||||
<span class="legend_label">{{ fieldset.label }}</span>
|
||||
{% if fieldset.description %}
|
||||
<i class="fa fa-info-circle" data-toggle="tooltip"
|
||||
@@ -106,9 +104,9 @@
|
||||
title="{{ fieldset.description }}"></i>
|
||||
{% endif %}
|
||||
<span class="collapser"> [-]</span>
|
||||
</legend>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="fieldset_content">
|
||||
<div class="panel-body">
|
||||
{% for fieldname in fieldset.fields %}
|
||||
{% if not fieldname %}
|
||||
<hr/>
|
||||
@@ -129,17 +127,17 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<fieldset class="fs-style">
|
||||
<legend class="legend-style">
|
||||
<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>
|
||||
</legend>
|
||||
<div class="fieldset_content">
|
||||
</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">
|
||||
@@ -157,7 +155,7 @@
|
||||
<span>Add filter</span>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{{ form.slice_id() }}
|
||||
{{ form.slice_name() }}
|
||||
{{ form.collapsed_fieldsets() }}
|
||||
|
||||
@@ -10,25 +10,51 @@
|
||||
{% block body %}
|
||||
<div class="container welcome">
|
||||
<div class="header">
|
||||
<h3><i class='fa fa-star'></i> Welcome!</h3>
|
||||
<h3>Welcome!</h3>
|
||||
</div>
|
||||
<hr/>
|
||||
<div id="cal-heatmap"></div>
|
||||
<hr/>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="widget">
|
||||
<div class="header"><h4><i class="fa fa-dashboard"></i> Dashboards</h4></div>
|
||||
<div>
|
||||
<table id="dash_table" class="table"></table>
|
||||
<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="widget">
|
||||
<div class="header"><h4><i class="fa fa-bar-chart"></i> Slices</h4></div>
|
||||
<div>
|
||||
<table id="slice_table" class="table"></table>
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Utility functions used across Caravel"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
import hashlib
|
||||
import functools
|
||||
import json
|
||||
import logging
|
||||
@@ -10,6 +13,7 @@ from dateutil.parser import parse
|
||||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
from markdown import markdown as md
|
||||
import parsedatetime
|
||||
from flask import Markup
|
||||
from flask_appbuilder.security.sqla import models as ab_models
|
||||
|
||||
|
||||
@@ -132,43 +136,6 @@ class JSONEncodedDict(TypeDecorator):
|
||||
return value
|
||||
|
||||
|
||||
class ColorFactory(object):
|
||||
|
||||
"""Used to generated arrays of colors server side"""
|
||||
|
||||
BNB_COLORS = [
|
||||
# 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',
|
||||
]
|
||||
|
||||
def __init__(self, hash_based=False):
|
||||
self.d = {}
|
||||
self.hash_based = hash_based
|
||||
|
||||
def get(self, s):
|
||||
"""Gets a color from a string and memoize the association
|
||||
|
||||
>>> cf = ColorFactory()
|
||||
>>> cf.get('item_1')
|
||||
'#ff5a5f'
|
||||
>>> cf.get('item_2')
|
||||
'#7b0051'
|
||||
>>> cf.get('item_1')
|
||||
'#ff5a5f'
|
||||
"""
|
||||
if self.hash_based:
|
||||
s = s.encode('utf-8')
|
||||
h = hashlib.md5(s)
|
||||
i = int(h.hexdigest(), 16)
|
||||
else:
|
||||
if s not in self.d:
|
||||
self.d[s] = len(self.d)
|
||||
i = self.d[s]
|
||||
return self.BNB_COLORS[i % len(self.BNB_COLORS)]
|
||||
|
||||
|
||||
def init(caravel):
|
||||
"""Inits the Caravel application with security roles and such"""
|
||||
db = caravel.db
|
||||
@@ -241,13 +208,16 @@ def json_iso_dttm_ser(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def markdown(s):
|
||||
def markdown(s, markup_wrap=False):
|
||||
s = s or ''
|
||||
return md(s, [
|
||||
s = md(s, [
|
||||
'markdown.extensions.tables',
|
||||
'markdown.extensions.fenced_code',
|
||||
'markdown.extensions.codehilite',
|
||||
])
|
||||
if markup_wrap:
|
||||
s = Markup(s)
|
||||
return s
|
||||
|
||||
|
||||
def readfile(filepath):
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
"""Flask web views for Caravel"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
@@ -36,6 +40,15 @@ def validate_json(form, field): # noqa
|
||||
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)
|
||||
@@ -117,7 +130,8 @@ 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']
|
||||
add_columns = [
|
||||
'database_name', 'sqlalchemy_uri', 'cache_timeout', 'extra']
|
||||
search_exclude_columns = ('password',)
|
||||
edit_columns = add_columns
|
||||
add_template = "caravel/models/database/add.html"
|
||||
@@ -127,7 +141,16 @@ class DatabaseView(CaravelModelView, DeleteMixin): # noqa
|
||||
'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")
|
||||
"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):
|
||||
@@ -213,6 +236,7 @@ if config['DRUID_IS_ACTIVE']:
|
||||
|
||||
class SliceModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
datamodel = SQLAInterface(models.Slice)
|
||||
add_template = "caravel/add_slice.html"
|
||||
can_add = False
|
||||
label_columns = {
|
||||
'created_by_': 'Creator',
|
||||
@@ -220,8 +244,8 @@ class SliceModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
}
|
||||
list_columns = [
|
||||
'slice_link', 'viz_type',
|
||||
'datasource_link', 'created_by_', 'changed_on']
|
||||
order_columns = utils.list_minus(list_columns, ['created_by_'])
|
||||
'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']
|
||||
@@ -248,6 +272,9 @@ class SliceAsync(SliceModelView): # noqa
|
||||
'created_by_', 'modified', 'icons']
|
||||
label_columns = {
|
||||
'icons': ' ',
|
||||
'created_by_': 'Creator',
|
||||
'viz_type': 'Type',
|
||||
'slice_link': 'Slice',
|
||||
}
|
||||
|
||||
appbuilder.add_view_no_menu(SliceAsync)
|
||||
@@ -258,8 +285,8 @@ class DashboardModelView(CaravelModelView, DeleteMixin): # noqa
|
||||
label_columns = {
|
||||
'created_by_': 'Creator',
|
||||
}
|
||||
list_columns = ['dashboard_link', 'created_by_', 'changed_on']
|
||||
order_columns = utils.list_minus(list_columns, ['created_by_'])
|
||||
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']
|
||||
@@ -298,6 +325,10 @@ appbuilder.add_view(
|
||||
|
||||
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)
|
||||
|
||||
@@ -388,6 +419,12 @@ class R(BaseView):
|
||||
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)
|
||||
|
||||
|
||||
@@ -402,12 +439,14 @@ class Caravel(BaseView):
|
||||
def explore(self, datasource_type, datasource_id):
|
||||
datasource_class = models.SqlaTable \
|
||||
if datasource_type == "table" else models.DruidDatasource
|
||||
datasource = (
|
||||
datasources = (
|
||||
db.session
|
||||
.query(datasource_class)
|
||||
.filter_by(id=datasource_id)
|
||||
.first()
|
||||
.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:
|
||||
@@ -417,7 +456,7 @@ class Caravel(BaseView):
|
||||
.first()
|
||||
)
|
||||
if not datasource:
|
||||
flash("The datasource seem to have been deleted", "alert")
|
||||
flash("The datasource seems to have been deleted", "alert")
|
||||
|
||||
all_datasource_access = self.appbuilder.sm.has_access(
|
||||
'all_datasource_access', 'all_datasource_access')
|
||||
@@ -453,6 +492,7 @@ class Caravel(BaseView):
|
||||
resp = Response(
|
||||
payload,
|
||||
status=status,
|
||||
headers=generate_download_headers("json"),
|
||||
mimetype="application/json")
|
||||
return resp
|
||||
elif request.args.get("csv") == "true":
|
||||
@@ -461,6 +501,7 @@ class Caravel(BaseView):
|
||||
return Response(
|
||||
payload,
|
||||
status=status,
|
||||
headers=generate_download_headers("csv"),
|
||||
mimetype="application/csv")
|
||||
else:
|
||||
if request.args.get("standalone") == "true":
|
||||
@@ -468,7 +509,8 @@ class Caravel(BaseView):
|
||||
else:
|
||||
template = "caravel/explore.html"
|
||||
|
||||
resp = self.render_template(template, viz=obj, slice=slc)
|
||||
resp = self.render_template(
|
||||
template, viz=obj, slice=slc, datasources=datasources)
|
||||
try:
|
||||
pass
|
||||
except Exception as e:
|
||||
@@ -633,7 +675,6 @@ class Caravel(BaseView):
|
||||
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
|
||||
@@ -760,7 +801,7 @@ class Caravel(BaseView):
|
||||
"[" + cluster.cluster_name + "]",
|
||||
'info')
|
||||
session.commit()
|
||||
return redirect("/datasourcemodelview/list/")
|
||||
return redirect("/druiddatasourcemodelview/list/")
|
||||
|
||||
@expose("/autocomplete/<datasource>/<column>/")
|
||||
def autocomplete(self, datasource, column):
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
These objects represent the backend of all the visualizations that
|
||||
Caravel can render.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from collections import OrderedDict, defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import hashlib
|
||||
|
||||
from flask import flash, request, Markup
|
||||
from markdown import markdown
|
||||
@@ -42,6 +47,8 @@ class BaseViz(object):
|
||||
|
||||
def __init__(self, datasource, form_data, slice=None):
|
||||
self.orig_form_data = form_data
|
||||
if not datasource:
|
||||
raise Exception("Viz is missing a datasource")
|
||||
self.datasource = datasource
|
||||
self.request = request
|
||||
self.viz_type = form_data.get("viz_type")
|
||||
@@ -240,7 +247,7 @@ class BaseViz(object):
|
||||
'data': self.get_data(),
|
||||
'query': self.query,
|
||||
'form_data': self.form_data,
|
||||
'json_endpoint': cache_key,
|
||||
'json_endpoint': self.json_endpoint,
|
||||
'csv_endpoint': self.csv_endpoint,
|
||||
'standalone_endpoint': self.standalone_endpoint,
|
||||
'cache_timeout': cache_timeout,
|
||||
@@ -254,7 +261,8 @@ class BaseViz(object):
|
||||
|
||||
def get_csv(self):
|
||||
df = self.get_df()
|
||||
return df.to_csv(index=False)
|
||||
include_index = not isinstance(df.index, pd.RangeIndex)
|
||||
return df.to_csv(index=include_index)
|
||||
|
||||
def get_data(self):
|
||||
return []
|
||||
@@ -265,7 +273,8 @@ class BaseViz(object):
|
||||
|
||||
@property
|
||||
def cache_key(self):
|
||||
return self.get_url(json="true", force="false")
|
||||
url = self.get_url(json="true", force="false")
|
||||
return hashlib.md5(url.encode('utf-8')).hexdigest()
|
||||
|
||||
@property
|
||||
def csv_endpoint(self):
|
||||
@@ -404,7 +413,7 @@ class PivotTableViz(BaseViz):
|
||||
na_rep='',
|
||||
classes=(
|
||||
"dataframe table table-striped table-bordered "
|
||||
"table-condensed table-hover"))
|
||||
"table-condensed table-hover").split(" "))
|
||||
|
||||
|
||||
class MarkupViz(BaseViz):
|
||||
@@ -549,7 +558,7 @@ class BigNumberViz(BaseViz):
|
||||
"""Put emphasis on a single metric with this big number viz"""
|
||||
|
||||
viz_type = "big_number"
|
||||
verbose_name = "Big Number"
|
||||
verbose_name = "Big Number with Trendline"
|
||||
is_timeseries = True
|
||||
fieldsets = ({
|
||||
'label': None,
|
||||
@@ -593,6 +602,51 @@ class BigNumberViz(BaseViz):
|
||||
}
|
||||
|
||||
|
||||
class BigNumberTotalViz(BaseViz):
|
||||
|
||||
"""Put emphasis on a single metric with this big number viz"""
|
||||
|
||||
viz_type = "big_number_total"
|
||||
verbose_name = "Big Number"
|
||||
is_timeseries = False
|
||||
fieldsets = ({
|
||||
'label': None,
|
||||
'fields': (
|
||||
'metric',
|
||||
'subheader',
|
||||
'y_axis_format',
|
||||
)
|
||||
},)
|
||||
form_overrides = {
|
||||
'y_axis_format': {
|
||||
'label': 'Number format',
|
||||
}
|
||||
}
|
||||
|
||||
def reassignments(self):
|
||||
metric = self.form_data.get('metric')
|
||||
if not metric:
|
||||
self.form_data['metric'] = self.orig_form_data.get('metrics')
|
||||
|
||||
def query_obj(self):
|
||||
d = super(BigNumberTotalViz, self).query_obj()
|
||||
metric = self.form_data.get('metric')
|
||||
if not metric:
|
||||
raise Exception("Pick a metric!")
|
||||
d['metrics'] = [self.form_data.get('metric')]
|
||||
self.form_data['metric'] = metric
|
||||
return d
|
||||
|
||||
def get_data(self):
|
||||
form_data = self.form_data
|
||||
df = self.get_df()
|
||||
df = df.sort(columns=df.columns[0])
|
||||
return {
|
||||
'data': df.values.tolist(),
|
||||
'subheader': form_data.get('subheader', ''),
|
||||
}
|
||||
|
||||
|
||||
class NVD3TimeSeriesViz(NVD3Viz):
|
||||
|
||||
"""A rich line chart component with tons of options"""
|
||||
@@ -829,6 +883,7 @@ class DistributionBarViz(DistributionPieViz):
|
||||
'metrics',
|
||||
'row_limit',
|
||||
('show_legend', 'bar_stacked'),
|
||||
('y_axis_format', None),
|
||||
)
|
||||
},)
|
||||
form_overrides = {
|
||||
@@ -1280,6 +1335,7 @@ viz_types_list = [
|
||||
MarkupViz,
|
||||
WordCloudViz,
|
||||
BigNumberViz,
|
||||
BigNumberTotalViz,
|
||||
SunburstViz,
|
||||
DirectedForceViz,
|
||||
SankeyViz,
|
||||
|
||||
17
docs/_static/docs.css
vendored
@@ -6,6 +6,23 @@ div.navbar {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
#tutorial img {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 5px 5px 5px #888888;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#gallery img {
|
||||
border: 1px solid gray;
|
||||
box-shadow: 5px 5px 5px #888888;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.carousel img {
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
@@ -1,6 +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/ _build/html/_static/img/
|
||||
cp -r ../caravel/assets/images/ _static/img/
|
||||
rm -rf /tmp/caravel-docs
|
||||
cp -r _build/html /tmp/caravel-docs
|
||||
|
||||
@@ -188,7 +188,7 @@ html_show_sourcelink = False
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
html_show_copyright = False
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
|
||||
45
docs/gallery.rst
Normal file
@@ -0,0 +1,45 @@
|
||||
Gallery
|
||||
=======
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/line.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/bubble.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/table.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/pie.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/bar.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/sankey.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/word_cloud.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/filter.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/pivot_table.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/force_directed.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/percent_change.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/sunburst.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/stacked.png
|
||||
:scale: 25 %
|
||||
|
||||
.. image:: _static/img/viz_thumbnails/big_number.png
|
||||
:scale: 25 %
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.. image:: _static/img/caravel.jpg
|
||||
|
||||
.. warning:: This project used to be name Panoramix and has been renamed
|
||||
to Caravel in March 2016
|
||||
|
||||
@@ -8,17 +10,17 @@ Features
|
||||
---------
|
||||
|
||||
- A rich set of data visualizations, integrated from some of the best
|
||||
visualization libraries
|
||||
visualization libraries
|
||||
- Create and share simple dashboards
|
||||
- An extensible, high-granularity security/permission model allowing
|
||||
intricate rules on who can access individual features and the dataset
|
||||
intricate rules on who can access individual features and the dataset
|
||||
- Enterprise-ready authentication with integration with major authentication
|
||||
providers (database, OpenID, LDAP, OAuth & REMOTE_USER through
|
||||
Flask AppBuilder)
|
||||
providers (database, OpenID, LDAP, OAuth & REMOTE_USER through
|
||||
Flask AppBuilder)
|
||||
- A simple semantic layer, allowing users to control how data sources are
|
||||
displayed in the UI by defining which fields should show up in which
|
||||
drop-down and which aggregation and function metrics are made available
|
||||
to the user
|
||||
displayed in the UI by defining which fields should show up in which
|
||||
drop-down and which aggregation and function metrics are made available
|
||||
to the user
|
||||
- Integration with most RDBMS through SqlAlchemy
|
||||
- Deep integration with Druid.io
|
||||
|
||||
@@ -29,7 +31,9 @@ Contents
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
user_guide
|
||||
tutorial
|
||||
videos
|
||||
gallery
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
@@ -24,26 +24,30 @@ 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
|
||||
sudo apt-get install build-essential libssl-dev libffi-dev python-dev python-pip
|
||||
|
||||
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 openssl-devel
|
||||
**OSX**, system python is not recommended. brew's python also ships with pip ::
|
||||
|
||||
**OSX** ::
|
||||
|
||||
brew install pkg-config libffi openssl
|
||||
brew install pkg-config libffi openssl python
|
||||
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography
|
||||
|
||||
**Windows** isn't officially supported at this point, but if you want to
|
||||
attempt it: ::
|
||||
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
|
||||
|
||||
|
||||
Caravel installation and initialization
|
||||
---------------------------------------
|
||||
@@ -69,7 +73,7 @@ Follow these few simple steps to install Caravel.::
|
||||
|
||||
|
||||
After installation, you should be able to point your browser to the right
|
||||
hostname:port [http://localhost:8088](http://localhost:8088), login using
|
||||
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
|
||||
@@ -98,7 +102,11 @@ of the parameters you can copy / paste in that configuration module: ::
|
||||
# Your App secret key
|
||||
SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h'
|
||||
|
||||
# The SQLAlchemy connection string.
|
||||
# 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, ...).
|
||||
# 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'
|
||||
|
||||
# Flask-WTF flag for CSRF
|
||||
@@ -131,13 +139,52 @@ data source's configuration, to your database's and ultimately falls back
|
||||
into your global default defined in ``CACHE_CONFIG``.
|
||||
|
||||
|
||||
Deeper SQLAlchemy integration
|
||||
-----------------------------
|
||||
|
||||
It is possible to tweak the database connection information using the
|
||||
parameters exposed by SQLAlchemy. In the ``Database`` edit view, you will
|
||||
find an ``extra`` field as a ``JSON`` blob.
|
||||
|
||||
.. image:: _static/img/tutorial/add_db.png
|
||||
:scale: 50 %
|
||||
|
||||
This JSON string contains 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`` get unpacked into the
|
||||
`sqlalchemy.MetaData <http://docs.sqlalchemy.org/en/rel_1_0/core/metadata.html#sqlalchemy.schema.MetaData>`_ call. Refer to the SQLAlchemy docs for more information.
|
||||
|
||||
|
||||
Postgres & Redshift
|
||||
-------------------
|
||||
|
||||
Postgres and Redshift use the concept of **schema** as a logical entity
|
||||
on top of the **database**. For Caravel to connect to a specific schema,
|
||||
you can either specify it in the ``metadata_params`` key of the ``extra``
|
||||
JSON blob described above, or you can use a database user name to connect to
|
||||
the database that matches the schema name you are interested it.
|
||||
|
||||
|
||||
Druid
|
||||
-----
|
||||
|
||||
* From the UI, enter the information about your clusters in the
|
||||
``Admin->Clusters`` menu by hitting the + sign.
|
||||
``Admin->Clusters`` menu by hitting the + sign.
|
||||
|
||||
* Once the Druid cluster connection information is entered, hit the
|
||||
``Admin->Refresh Metadata`` menu item to populate
|
||||
``Admin->Refresh Metadata`` menu item to populate
|
||||
|
||||
* Navigate to your datasources
|
||||
|
||||
Note that you can run the ``caravel refresh_druid`` command to refresh the
|
||||
metadata from your Druid cluster(s)
|
||||
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Upgrading should be as straightforward as running::
|
||||
|
||||
pip install caravel --upgrade
|
||||
caravel db upgrade
|
||||
|
||||
100
docs/tutorial.rst
Normal file
@@ -0,0 +1,100 @@
|
||||
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
|
||||
query. First go to the database menu
|
||||
|
||||
.. image:: _static/img/tutorial/db_menu.png
|
||||
:scale: 30 %
|
||||
|
||||
Now click on the ``+`` button to add a new entry
|
||||
|
||||
.. image:: _static/img/tutorial/db_plus.png
|
||||
:scale: 30 %
|
||||
|
||||
Fill in an arbitrary reference name for the database, and you SQLAlchemy
|
||||
URI. To figure out how to construct your URI, check out the
|
||||
`SQLAlchemy documentation <http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html>`_.
|
||||
Then you can test your connection. If it works, you'll see a positive popup
|
||||
and list of the tables that SQLAlchemy has found for that URI.
|
||||
|
||||
.. image:: _static/img/tutorial/db_added.png
|
||||
:scale: 30 %
|
||||
|
||||
Once your database has been added, it's time to add your table. Navigate
|
||||
using the navigation bar at the top to ``Sources -> Tables`` and click the
|
||||
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
|
||||
which fields are metrics in dimensions. From the list view, edit the table
|
||||
that you just created by clicking the tiny pen icon.
|
||||
|
||||
.. image:: _static/img/tutorial/pen.png
|
||||
:scale: 30 %
|
||||
|
||||
Now you're in the table editor, click on the "List Table Column" tab,
|
||||
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
|
||||
shown in the explore view, and which metrics should be created. Make sure
|
||||
to inform Caravel 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``.
|
||||
|
||||
You should now be back in the ``Table List`` view. Click on the name of the
|
||||
table you just created. You enter the "Explore" view for your table.
|
||||
|
||||
.. image:: _static/img/tutorial/explore.png
|
||||
:scale: 30 %
|
||||
|
||||
The next step is to create a Slice. First, make sure to use a time filter
|
||||
that is relevant.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use some "natural language time expressions"
|
||||
either as relative (as in ``now``, ``4 weeks ago``, or ``1 year ago``) as well
|
||||
as hard date or time expressions (as in ``3015``, ``3016-01-01`` or
|
||||
``May``).
|
||||
|
||||
Alter the form's option and click ``Query`` until you get to an interesting
|
||||
cut of data, and click ``SAVE AS``, enter a name, and you just created your first
|
||||
slice.
|
||||
|
||||
.. image:: _static/img/tutorial/created.png
|
||||
:scale: 30 %
|
||||
|
||||
This slice is now accessible in the slice list from the
|
||||
``Menu -> Slices`` at any time. Note that this view is easily filterable and
|
||||
searchable.
|
||||
|
||||
.. image:: _static/img/tutorial/search.png
|
||||
:scale: 30 %
|
||||
|
||||
Now let's create a dashboard. A dashboard is simply a collection of slices
|
||||
with metadata around their sizes, positions, CSS style and a few other things.
|
||||
Navigate to the dashboard list view ``Menu -> Dashboard`` and click the plus
|
||||
(``+``) sign. In the form, enter a name and pick the slice you just created.
|
||||
|
||||
.. image:: _static/img/tutorial/new_dash.png
|
||||
:scale: 30 %
|
||||
|
||||
Hit ``Save``, you should be back in ``Menu -> Dashboard``. Now enter your
|
||||
new dashboard.
|
||||
|
||||
.. image:: _static/img/tutorial/in_new_dash.png
|
||||
:scale: 30 %
|
||||
|
||||
Here you are. You can now resize and move the different slice(s), style them
|
||||
in the CSS modal window, and save right from here. For now, renaming the
|
||||
dashboard or adding on a new slice is done through the dashboard edit view,
|
||||
which is the same form as you used when you originally created the dashboard,
|
||||
and is accessible by clicking the ``edit`` pen icon from the dashboard list
|
||||
view (``Menu -> Dashboards``)
|
||||
@@ -1,7 +1,7 @@
|
||||
User Guide
|
||||
==========
|
||||
Videos
|
||||
======
|
||||
|
||||
The user guide is a collection of short videos showing different aspect
|
||||
Here is a collection of short videos showing different aspect
|
||||
of Caravel.
|
||||
|
||||
Quick Intro
|
||||