`, and ``.
+@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace;
+@font-family-base: @font-family-sans-serif;
+
+@font-size-base: 13px;
+@font-size-large: ceil((@font-size-base * 1.25)); // ~18px
+@font-size-small: ceil((@font-size-base * 0.85)); // ~12px
+
+@font-size-h1: 56px;
+@font-size-h2: 45px;
+@font-size-h3: 34px;
+@font-size-h4: 24px;
+@font-size-h5: 20px;
+@font-size-h6: 14px;
+
+//** Unit-less `line-height` for use in components like buttons.
+@line-height-base: 1.846; // 20/14
+//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
+@line-height-computed: floor((@font-size-base * @line-height-base)); // ~20px
+
+//** By default, this inherits from the ``.
+@headings-font-family: inherit;
+@headings-font-weight: 400;
+@headings-line-height: 1.1;
+@headings-color: #444;
+
+
+//== Iconography
+//
+//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
+
+//** Load fonts from this directory.
+@icon-font-path: "../fonts/";
+//** File name for all font files.
+@icon-font-name: "glyphicons-halflings-regular";
+//** Element ID within SVG icon file.
+@icon-font-svg-id: "glyphicons_halflingsregular";
+
+
+//== Components
+//
+//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
+
+@padding-base-vertical: 6px;
+@padding-base-horizontal: 16px;
+
+@padding-large-vertical: 10px;
+@padding-large-horizontal: 16px;
+
+@padding-small-vertical: 5px;
+@padding-small-horizontal: 10px;
+
+@padding-xs-vertical: 1px;
+@padding-xs-horizontal: 5px;
+
+@line-height-large: 1.3333333; // extra decimals for Win 8.1 Chrome
+@line-height-small: 1.5;
+
+@border-radius-base: 3px;
+@border-radius-large: 3px;
+@border-radius-small: 3px;
+
+//** Global color for active items (e.g., navs or dropdowns).
+@component-active-color: #fff;
+//** Global background color for active items (e.g., navs or dropdowns).
+@component-active-bg: @brand-primary;
+
+//** Width of the `border` for generating carets that indicator dropdowns.
+@caret-width-base: 4px;
+//** Carets increase slightly in size for larger components.
+@caret-width-large: 5px;
+
+
+//== Tables
+//
+//## Customizes the `.table` component with basic values, each used across all table variations.
+
+//** Padding for ``s and ` `s.
+@table-cell-padding: 8px;
+//** Padding for cells in `.table-condensed`.
+@table-condensed-cell-padding: 5px;
+
+//** Default background color used for all tables.
+@table-bg: transparent;
+//** Background color used for `.table-striped`.
+@table-bg-accent: #f9f9f9;
+//** Background color used for `.table-hover`.
+@table-bg-hover: @gray-lighter;
+@table-bg-active: @table-bg-hover;
+
+//** Border color for table and cell borders.
+@table-border-color: #ddd;
+
+
+//== Buttons
+//
+//## For each of Bootstrap's buttons, define text, background and border color.
+
+@btn-font-weight: normal;
+
+@btn-default-color: #444;
+@btn-default-bg: #fff;
+@btn-default-border: transparent;
+
+@btn-primary-color: #fff;
+@btn-primary-bg: @brand-primary;
+@btn-primary-border: transparent;
+
+@btn-success-color: #fff;
+@btn-success-bg: @brand-success;
+@btn-success-border: transparent;
+
+@btn-info-color: #fff;
+@btn-info-bg: @brand-info;
+@btn-info-border: transparent;
+
+@btn-warning-color: #fff;
+@btn-warning-bg: @brand-warning;
+@btn-warning-border: transparent;
+
+@btn-danger-color: #fff;
+@btn-danger-bg: @brand-danger;
+@btn-danger-border: transparent;
+
+@btn-link-disabled-color: @gray-light;
+
+// Allows for customizing button radius independently from global border radius
+@btn-border-radius-base: @border-radius-base;
+@btn-border-radius-large: @border-radius-large;
+@btn-border-radius-small: @border-radius-small;
+
+
+//== Forms
+//
+//##
+
+//** ` ` background color
+@input-bg: transparent;
+//** ` ` background color
+@input-bg-disabled: transparent;
+
+//** Text color for ` `s
+@input-color: @gray;
+//** ` ` border color
+@input-border: transparent;
+
+// TODO: Rename `@input-border-radius` to `@input-border-radius-base` in v4
+//** Default `.form-control` border radius
+// This has no effect on ``s in some browsers, due to the limited stylability of ``s in CSS.
+@input-border-radius: @border-radius-base;
+//** Large `.form-control` border radius
+@input-border-radius-large: @border-radius-large;
+//** Small `.form-control` border radius
+@input-border-radius-small: @border-radius-small;
+
+//** Border color for inputs on focus
+@input-border-focus: #66afe9;
+
+//** Placeholder text color
+@input-color-placeholder: @gray-light;
+
+//** Default `.form-control` height
+@input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2);
+//** Large `.form-control` height
+@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2);
+//** Small `.form-control` height
+@input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2);
+
+//** `.form-group` margin
+@form-group-margin-bottom: 15px;
+
+@legend-color: @gray-dark;
+@legend-border-color: #e5e5e5;
+
+//** Background color for textual input addons
+@input-group-addon-bg: transparent;
+//** Border color for textual input addons
+@input-group-addon-border-color: @input-border;
+
+//** Disabled cursor for form controls and buttons.
+@cursor-disabled: not-allowed;
+
+
+//== Dropdowns
+//
+//## Dropdown menu container and contents.
+
+//** Background for the dropdown menu.
+@dropdown-bg: #fff;
+//** Dropdown menu `border-color`.
+@dropdown-border: rgba(0,0,0,.15);
+//** Dropdown menu `border-color` **for IE8**.
+@dropdown-fallback-border: #ccc;
+//** Divider color for between dropdown items.
+@dropdown-divider-bg: #e5e5e5;
+
+//** Dropdown link text color.
+@dropdown-link-color: @text-color;
+//** Hover color for dropdown links.
+@dropdown-link-hover-color: darken(@gray-dark, 5%);
+//** Hover background for dropdown links.
+@dropdown-link-hover-bg: @gray-lighter;
+
+//** Active dropdown menu item text color.
+@dropdown-link-active-color: @component-active-color;
+//** Active dropdown menu item background color.
+@dropdown-link-active-bg: @component-active-bg;
+
+//** Disabled dropdown menu item background color.
+@dropdown-link-disabled-color: @gray-light;
+
+//** Text color for headers within dropdown menus.
+@dropdown-header-color: @gray-light;
+
+//** Deprecated `@dropdown-caret-color` as of v3.1.0
+@dropdown-caret-color: @gray-light;
+
+
+//-- Z-index master list
+//
+// Warning: Avoid customizing these values. They're used for a bird's eye view
+// of components dependent on the z-axis and are designed to all work together.
+//
+// Note: These variables are not generated into the Customizer.
+
+@zindex-navbar: 1000;
+@zindex-dropdown: 1000;
+@zindex-popover: 1060;
+@zindex-tooltip: 1070;
+@zindex-navbar-fixed: 1030;
+@zindex-modal-background: 1040;
+@zindex-modal: 1050;
+
+
+//== Media queries breakpoints
+//
+//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
+
+// Extra small screen / phone
+//** Deprecated `@screen-xs` as of v3.0.1
+@screen-xs: 480px;
+//** Deprecated `@screen-xs-min` as of v3.2.0
+@screen-xs-min: @screen-xs;
+//** Deprecated `@screen-phone` as of v3.0.1
+@screen-phone: @screen-xs-min;
+
+// Small screen / tablet
+//** Deprecated `@screen-sm` as of v3.0.1
+@screen-sm: 768px;
+@screen-sm-min: @screen-sm;
+//** Deprecated `@screen-tablet` as of v3.0.1
+@screen-tablet: @screen-sm-min;
+
+// Medium screen / desktop
+//** Deprecated `@screen-md` as of v3.0.1
+@screen-md: 992px;
+@screen-md-min: @screen-md;
+//** Deprecated `@screen-desktop` as of v3.0.1
+@screen-desktop: @screen-md-min;
+
+// Large screen / wide desktop
+//** Deprecated `@screen-lg` as of v3.0.1
+@screen-lg: 1200px;
+@screen-lg-min: @screen-lg;
+//** Deprecated `@screen-lg-desktop` as of v3.0.1
+@screen-lg-desktop: @screen-lg-min;
+
+// So media queries don't overlap when required, provide a maximum
+@screen-xs-max: (@screen-sm-min - 1);
+@screen-sm-max: (@screen-md-min - 1);
+@screen-md-max: (@screen-lg-min - 1);
+
+
+//== Grid system
+//
+//## Define your custom responsive grid.
+
+//** Number of columns in the grid.
+@grid-columns: 12;
+//** Padding between columns. Gets divided in half for the left and right.
+@grid-gutter-width: 30px;
+// Navbar collapse
+//** Point at which the navbar becomes uncollapsed.
+@grid-float-breakpoint: @screen-sm-min;
+//** Point at which the navbar begins collapsing.
+@grid-float-breakpoint-max: (@grid-float-breakpoint - 1);
+
+
+//== Container sizes
+//
+//## Define the maximum width of `.container` for different screen sizes.
+
+// Small screen / tablet
+@container-tablet: (720px + @grid-gutter-width);
+//** For `@screen-sm-min` and up.
+@container-sm: @container-tablet;
+
+// Medium screen / desktop
+@container-desktop: (940px + @grid-gutter-width);
+//** For `@screen-md-min` and up.
+@container-md: @container-desktop;
+
+// Large screen / wide desktop
+@container-large-desktop: (1140px + @grid-gutter-width);
+//** For `@screen-lg-min` and up.
+@container-lg: @container-large-desktop;
+
+
+//== Navbar
+//
+//##
+
+// Basics of a navbar
+@navbar-height: 64px;
+@navbar-margin-bottom: @line-height-computed;
+@navbar-border-radius: @border-radius-base;
+@navbar-padding-horizontal: floor((@grid-gutter-width / 2));
+@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2);
+@navbar-collapse-max-height: 340px;
+
+@navbar-default-color: @gray-light;
+@navbar-default-bg: #fff;
+@navbar-default-border: transparent;
+
+// Navbar links
+@navbar-default-link-color: @gray;
+@navbar-default-link-hover-color: @gray-dark;
+@navbar-default-link-hover-bg: transparent;
+@navbar-default-link-active-color: @gray-dark;
+@navbar-default-link-active-bg: darken(@navbar-default-bg, 6.5%);
+@navbar-default-link-disabled-color: #ccc;
+@navbar-default-link-disabled-bg: transparent;
+
+// Navbar brand label
+@navbar-default-brand-color: @navbar-default-link-color;
+@navbar-default-brand-hover-color: @navbar-default-link-hover-color;
+@navbar-default-brand-hover-bg: transparent;
+
+// Navbar toggle
+@navbar-default-toggle-hover-bg: transparent;
+@navbar-default-toggle-icon-bar-bg: rgba(0,0,0,0.5);
+@navbar-default-toggle-border-color: transparent;
+
+
+//=== Inverted navbar
+// Reset inverted navbar basics
+@navbar-inverse-color: @gray-light;
+@navbar-inverse-bg: @brand-primary;
+@navbar-inverse-border: transparent;
+
+// Inverted navbar links
+@navbar-inverse-link-color: lighten(@brand-primary, 30%);
+@navbar-inverse-link-hover-color: #fff;
+@navbar-inverse-link-hover-bg: transparent;
+@navbar-inverse-link-active-color: @navbar-inverse-link-hover-color;
+@navbar-inverse-link-active-bg: darken(@navbar-inverse-bg, 10%);
+@navbar-inverse-link-disabled-color: #444;
+@navbar-inverse-link-disabled-bg: transparent;
+
+// Inverted navbar brand label
+@navbar-inverse-brand-color: @navbar-inverse-link-color;
+@navbar-inverse-brand-hover-color: #fff;
+@navbar-inverse-brand-hover-bg: transparent;
+
+// Inverted navbar toggle\
+@navbar-inverse-toggle-hover-bg: transparent;
+@navbar-inverse-toggle-icon-bar-bg: rgba(0,0,0,0.5);
+@navbar-inverse-toggle-border-color: transparent;
+
+
+//== Navs
+//
+//##
+
+//=== Shared nav styles
+@nav-link-padding: 10px 15px;
+@nav-link-hover-bg: @gray-lighter;
+
+@nav-disabled-link-color: @gray-light;
+@nav-disabled-link-hover-color: @gray-light;
+
+//== Tabs
+@nav-tabs-border-color: transparent;
+
+@nav-tabs-link-hover-border-color: @gray-lighter;
+
+@nav-tabs-active-link-hover-bg: transparent;
+@nav-tabs-active-link-hover-color: @gray;
+@nav-tabs-active-link-hover-border-color: transparent;
+
+@nav-tabs-justified-link-border-color: @nav-tabs-border-color;
+@nav-tabs-justified-active-link-border-color: @body-bg;
+
+//== Pills
+@nav-pills-border-radius: @border-radius-base;
+@nav-pills-active-link-hover-bg: @component-active-bg;
+@nav-pills-active-link-hover-color: @component-active-color;
+
+
+//== Pagination
+//
+//##
+
+@pagination-color: @link-color;
+@pagination-bg: #fff;
+@pagination-border: #ddd;
+
+@pagination-hover-color: @link-hover-color;
+@pagination-hover-bg: @gray-lighter;
+@pagination-hover-border: #ddd;
+
+@pagination-active-color: #fff;
+@pagination-active-bg: @brand-primary;
+@pagination-active-border: @brand-primary;
+
+@pagination-disabled-color: @gray-light;
+@pagination-disabled-bg: #fff;
+@pagination-disabled-border: #ddd;
+
+
+//== Pager
+//
+//##
+
+@pager-bg: @pagination-bg;
+@pager-border: @pagination-border;
+@pager-border-radius: 15px;
+
+@pager-hover-bg: @pagination-hover-bg;
+
+@pager-active-bg: @pagination-active-bg;
+@pager-active-color: @pagination-active-color;
+
+@pager-disabled-color: @pagination-disabled-color;
+
+
+//== Jumbotron
+//
+//##
+
+@jumbotron-padding: 30px;
+@jumbotron-color: inherit;
+@jumbotron-bg: #f9f9f9;
+@jumbotron-heading-color: @headings-color;
+@jumbotron-font-size: ceil((@font-size-base * 1.5));
+@jumbotron-heading-font-size: ceil((@font-size-base * 4.5));
+
+
+//== Form states and alerts
+//
+//## Define colors for form feedback states and, by default, alerts.
+
+@state-success-text: @brand-success;
+@state-success-bg: #dff0d8;
+@state-success-border: darken(spin(@state-success-bg, -10), 5%);
+
+@state-info-text: @brand-info;
+@state-info-bg: #e1bee7;
+@state-info-border: darken(spin(@state-info-bg, -10), 7%);
+
+@state-warning-text: @brand-warning;
+@state-warning-bg: #ffe0b2;
+@state-warning-border: darken(spin(@state-warning-bg, -10), 5%);
+
+@state-danger-text: @brand-danger;
+@state-danger-bg: #f9bdbb;
+@state-danger-border: darken(spin(@state-danger-bg, -10), 5%);
+
+
+//== Tooltips
+//
+//##
+
+//** Tooltip max width
+@tooltip-max-width: 200px;
+//** Tooltip text color
+@tooltip-color: #fff;
+//** Tooltip background color
+@tooltip-bg: #727272;
+@tooltip-opacity: .9;
+
+//** Tooltip arrow width
+@tooltip-arrow-width: 5px;
+//** Tooltip arrow color
+@tooltip-arrow-color: @tooltip-bg;
+
+
+//== Popovers
+//
+//##
+
+//** Popover body background color
+@popover-bg: #fff;
+//** Popover maximum width
+@popover-max-width: 276px;
+//** Popover border color
+@popover-border-color: transparent;
+//** Popover fallback border color
+@popover-fallback-border-color: transparent;
+
+//** Popover title background color
+@popover-title-bg: darken(@popover-bg, 3%);
+
+//** Popover arrow width
+@popover-arrow-width: 10px;
+//** Popover arrow color
+@popover-arrow-color: @popover-bg;
+
+//** Popover outer arrow width
+@popover-arrow-outer-width: (@popover-arrow-width + 1);
+//** Popover outer arrow color
+@popover-arrow-outer-color: fadein(@popover-border-color, 7.5%);
+//** Popover outer arrow fallback color
+@popover-arrow-outer-fallback-color: darken(@popover-fallback-border-color, 20%);
+
+
+//== Labels
+//
+//##
+
+//** Default label background color
+@label-default-bg: @gray-light;
+//** Primary label background color
+@label-primary-bg: @brand-primary;
+//** Success label background color
+@label-success-bg: @brand-success;
+//** Info label background color
+@label-info-bg: @brand-info;
+//** Warning label background color
+@label-warning-bg: @brand-warning;
+//** Danger label background color
+@label-danger-bg: @brand-danger;
+
+//** Default label text color
+@label-color: #fff;
+//** Default text color of a linked label
+@label-link-hover-color: #fff;
+
+
+//== Modals
+//
+//##
+
+//** Padding applied to the modal body
+@modal-inner-padding: 15px;
+
+//** Padding applied to the modal title
+@modal-title-padding: 15px;
+//** Modal title line-height
+@modal-title-line-height: @line-height-base;
+
+//** Background color of modal content area
+@modal-content-bg: #fff;
+//** Modal content border color
+@modal-content-border-color: transparent;
+//** Modal content border color **for IE8**
+@modal-content-fallback-border-color: #999;
+
+//** Modal backdrop background color
+@modal-backdrop-bg: #000;
+//** Modal backdrop opacity
+@modal-backdrop-opacity: .5;
+//** Modal header border color
+@modal-header-border-color: transparent;
+//** Modal footer border color
+@modal-footer-border-color: @modal-header-border-color;
+
+@modal-lg: 900px;
+@modal-md: 600px;
+@modal-sm: 300px;
+
+
+//== Alerts
+//
+//## Define alert colors, border radius, and padding.
+
+@alert-padding: 15px;
+@alert-border-radius: @border-radius-base;
+@alert-link-font-weight: bold;
+
+@alert-success-bg: @state-success-bg;
+@alert-success-text: @state-success-text;
+@alert-success-border: @state-success-border;
+
+@alert-info-bg: @state-info-bg;
+@alert-info-text: @state-info-text;
+@alert-info-border: @state-info-border;
+
+@alert-warning-bg: @state-warning-bg;
+@alert-warning-text: @state-warning-text;
+@alert-warning-border: @state-warning-border;
+
+@alert-danger-bg: @state-danger-bg;
+@alert-danger-text: @state-danger-text;
+@alert-danger-border: @state-danger-border;
+
+
+//== Progress bars
+//
+//##
+
+//** Background color of the whole progress component
+@progress-bg: #f5f5f5;
+//** Progress bar text color
+@progress-bar-color: #fff;
+//** Variable for setting rounded corners on progress bar.
+@progress-border-radius: @border-radius-base;
+
+//** Default progress bar color
+@progress-bar-bg: @brand-primary;
+//** Success progress bar color
+@progress-bar-success-bg: @brand-success;
+//** Warning progress bar color
+@progress-bar-warning-bg: @brand-warning;
+//** Danger progress bar color
+@progress-bar-danger-bg: @brand-danger;
+//** Info progress bar color
+@progress-bar-info-bg: @brand-info;
+
+
+//== List group
+//
+//##
+
+//** Background color on `.list-group-item`
+@list-group-bg: #fff;
+//** `.list-group-item` border color
+@list-group-border: #ddd;
+//** List group border radius
+@list-group-border-radius: @border-radius-base;
+
+//** Background color of single list items on hover
+@list-group-hover-bg: #f5f5f5;
+//** Text color of active list items
+@list-group-active-color: @component-active-color;
+//** Background color of active list items
+@list-group-active-bg: @component-active-bg;
+//** Border color of active list elements
+@list-group-active-border: @list-group-active-bg;
+//** Text color for content within active list items
+@list-group-active-text-color: lighten(@list-group-active-bg, 40%);
+
+//** Text color of disabled list items
+@list-group-disabled-color: @gray-light;
+//** Background color of disabled list items
+@list-group-disabled-bg: @gray-lighter;
+//** Text color for content within disabled list items
+@list-group-disabled-text-color: @list-group-disabled-color;
+
+@list-group-link-color: #555;
+@list-group-link-hover-color: @list-group-link-color;
+@list-group-link-heading-color: #333;
+
+
+//== Panels
+//
+//##
+
+@panel-bg: #fff;
+@panel-body-padding: 15px;
+@panel-heading-padding: 10px 15px;
+@panel-footer-padding: @panel-heading-padding;
+@panel-border-radius: @border-radius-base;
+
+//** Border color for elements within panels
+@panel-inner-border: #ddd;
+@panel-footer-bg: #f5f5f5;
+
+@panel-default-text: @gray-dark;
+@panel-default-border: #ddd;
+@panel-default-heading-bg: #f5f5f5;
+
+@panel-primary-text: #fff;
+@panel-primary-border: @brand-primary;
+@panel-primary-heading-bg: @brand-primary;
+
+@panel-success-text: #fff;
+@panel-success-border: @state-success-border;
+@panel-success-heading-bg: @brand-success;
+
+@panel-info-text: #fff;
+@panel-info-border: @state-info-border;
+@panel-info-heading-bg: @brand-info;
+
+@panel-warning-text: #fff;
+@panel-warning-border: @state-warning-border;
+@panel-warning-heading-bg: @brand-warning;
+
+@panel-danger-text: #fff;
+@panel-danger-border: @state-danger-border;
+@panel-danger-heading-bg: @brand-danger;
+
+
+//== Thumbnails
+//
+//##
+
+//** Padding around the thumbnail image
+@thumbnail-padding: 4px;
+//** Thumbnail background color
+@thumbnail-bg: @body-bg;
+//** Thumbnail border color
+@thumbnail-border: #ddd;
+//** Thumbnail border radius
+@thumbnail-border-radius: @border-radius-base;
+
+//** Custom text color for thumbnail captions
+@thumbnail-caption-color: @text-color;
+//** Padding around the thumbnail caption
+@thumbnail-caption-padding: 9px;
+
+
+//== Wells
+//
+//##
+
+@well-bg: #f9f9f9;
+@well-border: transparent;
+
+
+//== Badges
+//
+//##
+
+@badge-color: #fff;
+//** Linked badge text color on hover
+@badge-link-hover-color: #fff;
+@badge-bg: @gray-light;
+
+//** Badge text color in active nav link
+@badge-active-color: @link-color;
+//** Badge background color in active nav link
+@badge-active-bg: #fff;
+
+@badge-font-weight: normal;
+@badge-line-height: 1;
+@badge-border-radius: 10px;
+
+
+//== Breadcrumbs
+//
+//##
+
+@breadcrumb-padding-vertical: 8px;
+@breadcrumb-padding-horizontal: 15px;
+//** Breadcrumb background color
+@breadcrumb-bg: #f5f5f5;
+//** Breadcrumb text color
+@breadcrumb-color: #ccc;
+//** Text color of current page in the breadcrumb
+@breadcrumb-active-color: @gray-light;
+//** Textual separator for between breadcrumb elements
+@breadcrumb-separator: "/";
+
+
+//== Carousel
+//
+//##
+
+@carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6);
+
+@carousel-control-color: #fff;
+@carousel-control-width: 15%;
+@carousel-control-opacity: .5;
+@carousel-control-font-size: 20px;
+
+@carousel-indicator-active-bg: #fff;
+@carousel-indicator-border-color: #fff;
+
+@carousel-caption-color: #fff;
+
+
+//== Close
+//
+//##
+
+@close-font-weight: normal;
+@close-color: #000;
+@close-text-shadow: none;
+
+
+//== Code
+//
+//##
+
+@code-color: #c7254e;
+@code-bg: #f9f2f4;
+
+@kbd-color: #fff;
+@kbd-bg: #333;
+
+@pre-bg: #f5f5f5;
+@pre-color: @gray-dark;
+@pre-border-color: #ccc;
+@pre-scrollable-max-height: 340px;
+
+
+//== Type
+//
+//##
+
+//** Horizontal offset for forms and lists.
+@component-offset-horizontal: 180px;
+//** Text muted color
+@text-muted: @gray-light;
+//** Abbreviations and acronyms border color
+@abbr-border-color: @gray-light;
+//** Headings small color
+@headings-small-color: @gray-light;
+//** Blockquote small color
+@blockquote-small-color: @gray-light;
+//** Blockquote font size
+@blockquote-font-size: (@font-size-base * 1.25);
+//** Blockquote border color
+@blockquote-border-color: @gray-lighter;
+//** Page header border color
+@page-header-border-color: @gray-lighter;
+//** Width of horizontal description list titles
+@dl-horizontal-offset: @component-offset-horizontal;
+//** Point at which .dl-horizontal becomes horizontal
+@dl-horizontal-breakpoint: @grid-float-breakpoint;
+//** Horizontal line color.
+@hr-border: @gray-lighter;
diff --git a/dashed/assets/vendor/parallel_coordinates/d3.parcoords.css b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.css
new file mode 100644
index 000000000..b53849c36
--- /dev/null
+++ b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.css
@@ -0,0 +1,71 @@
+.parcoords svg, .parcoords canvas {
+ font-size: 12px;
+ position: absolute;
+}
+.parcoords > canvas {
+ pointer-events: none;
+}
+
+.parcoords text.label {
+ font: 100%;
+ font-size: 12px;
+ cursor: drag;
+}
+
+.parcoords rect.background {
+ fill: transparent;
+}
+.parcoords rect.background:hover {
+ fill: rgba(120,120,120,0.2);
+}
+.parcoords .resize rect {
+ fill: rgba(0,0,0,0.1);
+}
+.parcoords rect.extent {
+ fill: rgba(255,255,255,0.25);
+ stroke: rgba(0,0,0,0.6);
+}
+.parcoords .axis line, .parcoords .axis path {
+ fill: none;
+ stroke: #222;
+ shape-rendering: crispEdges;
+}
+.parcoords canvas {
+ opacity: 1;
+ -moz-transition: opacity 0.3s;
+ -webkit-transition: opacity 0.3s;
+ -o-transition: opacity 0.3s;
+}
+.parcoords canvas.faded {
+ opacity: 0.25;
+}
+.parcoords {
+ -webkit-touch-callout: none;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ background-color: white;
+}
+
+/* data table styles */
+.parcoords .row, .parcoords .header {
+ clear: left; font-size: 12px; line-height: 18px; height: 18px;
+ margin: 0px;
+}
+.parcoords .row:nth-child(odd) {
+ background: rgba(0,0,0,0.05);
+}
+.parcoords .header {
+ font-weight: bold;
+}
+.parcoords .cell {
+ float: left;
+ overflow: hidden;
+ white-space: nowrap;
+ width: 100px; height: 18px;
+}
+.parcoords .col-0 {
+ width: 180px;
+}
diff --git a/dashed/assets/vendor/parallel_coordinates/d3.parcoords.js b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.js
new file mode 100644
index 000000000..04095f106
--- /dev/null
+++ b/dashed/assets/vendor/parallel_coordinates/d3.parcoords.js
@@ -0,0 +1,2224 @@
+module.exports = function(config) {
+ var __ = {
+ data: [],
+ highlighted: [],
+ dimensions: [],
+ dimensionTitles: {},
+ dimensionTitleRotation: 0,
+ types: {},
+ brushed: false,
+ brushedColor: null,
+ alphaOnBrushed: 0.0,
+ mode: "default",
+ rate: 20,
+ width: 600,
+ height: 300,
+ margin: { top: 24, right: 0, bottom: 12, left: 0 },
+ nullValueSeparator: "undefined", // set to "top" or "bottom"
+ nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 },
+ color: "#069",
+ composite: "source-over",
+ alpha: 0.7,
+ bundlingStrength: 0.5,
+ bundleDimension: null,
+ smoothness: 0.0,
+ showControlPoints: false,
+ hideAxis : []
+ };
+
+ extend(__, config);
+
+ var pc = function(selection) {
+ selection = pc.selection = d3.select(selection);
+
+ __.width = selection[0][0].clientWidth;
+ __.height = selection[0][0].clientHeight;
+
+ // canvas data layers
+ ["marks", "foreground", "brushed", "highlight"].forEach(function(layer) {
+ canvas[layer] = selection
+ .append("canvas")
+ .attr("class", layer)[0][0];
+ ctx[layer] = canvas[layer].getContext("2d");
+ });
+
+ // svg tick and brush layers
+ pc.svg = selection
+ .append("svg")
+ .attr("width", __.width)
+ .attr("height", __.height)
+ .append("svg:g")
+ .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ return pc;
+ };
+ var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "axesreorder"].concat(d3.keys(__))),
+ w = function() { return __.width - __.margin.right - __.margin.left; },
+ h = function() { return __.height - __.margin.top - __.margin.bottom; },
+ flags = {
+ brushable: false,
+ reorderable: false,
+ axes: false,
+ interactive: false,
+ debug: false
+ },
+ xscale = d3.scale.ordinal(),
+ yscale = {},
+ dragging = {},
+ line = d3.svg.line(),
+ axis = d3.svg.axis().orient("left").ticks(5),
+ g, // groups for axes, brushes
+ ctx = {},
+ canvas = {},
+ clusterCentroids = [];
+
+ // side effects for setters
+ var side_effects = d3.dispatch.apply(this,d3.keys(__))
+ .on("composite", function(d) {
+ ctx.foreground.globalCompositeOperation = d.value;
+ ctx.brushed.globalCompositeOperation = d.value;
+ })
+ .on("alpha", function(d) {
+ ctx.foreground.globalAlpha = d.value;
+ ctx.brushed.globalAlpha = d.value;
+ })
+ .on("brushedColor", function (d) {
+ ctx.brushed.strokeStyle = d.value;
+ })
+ .on("width", function(d) { pc.resize(); })
+ .on("height", function(d) { pc.resize(); })
+ .on("margin", function(d) { pc.resize(); })
+ .on("rate", function(d) {
+ brushedQueue.rate(d.value);
+ foregroundQueue.rate(d.value);
+ })
+ .on("dimensions", function(d) {
+ xscale.domain(__.dimensions);
+ if (flags.interactive){pc.render().updateAxes();}
+ })
+ .on("bundleDimension", function(d) {
+ if (!__.dimensions.length) pc.detectDimensions();
+ if (!(__.dimensions[0] in yscale)) pc.autoscale();
+ if (typeof d.value === "number") {
+ if (d.value < __.dimensions.length) {
+ __.bundleDimension = __.dimensions[d.value];
+ } else if (d.value < __.hideAxis.length) {
+ __.bundleDimension = __.hideAxis[d.value];
+ }
+ } else {
+ __.bundleDimension = d.value;
+ }
+
+ __.clusterCentroids = compute_cluster_centroids(__.bundleDimension);
+ })
+ .on("hideAxis", function(d) {
+ if (!__.dimensions.length) pc.detectDimensions();
+ pc.dimensions(without(__.dimensions, d.value));
+ });
+
+ // expose the state of the chart
+ pc.state = __;
+ pc.flags = flags;
+
+ // create getter/setters
+ getset(pc, __, events);
+
+ // expose events
+ d3.rebind(pc, events, "on");
+
+ // getter/setter with event firing
+ function getset(obj,state,events) {
+ d3.keys(state).forEach(function(key) {
+ obj[key] = function(x) {
+ if (!arguments.length) {
+ return state[key];
+ }
+ var old = state[key];
+ state[key] = x;
+ side_effects[key].call(pc,{"value": x, "previous": old});
+ events[key].call(pc,{"value": x, "previous": old});
+ return obj;
+ };
+ });
+ };
+
+ function extend(target, source) {
+ for (var key in source) {
+ target[key] = source[key];
+ }
+ return target;
+ };
+
+ function without(arr, item) {
+ return arr.filter(function(elem) { return item.indexOf(elem) === -1; })
+ };
+ /** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */
+ function getRange() {
+ if (__.nullValueSeparator=="bottom") {
+ return [h()+1-__.nullValueSeparatorPadding.bottom-__.nullValueSeparatorPadding.top, 1];
+ } else if (__.nullValueSeparator=="top") {
+ return [h()+1, 1+__.nullValueSeparatorPadding.bottom+__.nullValueSeparatorPadding.top];
+ }
+ return [h()+1, 1];
+ };
+
+ pc.autoscale = function() {
+ // yscale
+ var defaultScales = {
+ "date": function(k) {
+ var extent = d3.extent(__.data, function(d) {
+ return d[k] ? d[k].getTime() : null;
+ });
+
+ // special case if single value
+ if (extent[0] === extent[1]) {
+ return d3.scale.ordinal()
+ .domain([extent[0]])
+ .rangePoints(getRange());
+ }
+
+ return d3.time.scale()
+ .domain(extent)
+ .range(getRange());
+ },
+ "number": function(k) {
+ var extent = d3.extent(__.data, function(d) { return +d[k]; });
+
+ // special case if single value
+ if (extent[0] === extent[1]) {
+ return d3.scale.ordinal()
+ .domain([extent[0]])
+ .rangePoints(getRange());
+ }
+
+ return d3.scale.linear()
+ .domain(extent)
+ .range(getRange());
+ },
+ "string": function(k) {
+ var counts = {},
+ domain = [];
+
+ // Let's get the count for each value so that we can sort the domain based
+ // on the number of items for each value.
+ __.data.map(function(p) {
+ if (p[k] === undefined && __.nullValueSeparator!== "undefined"){
+ return; // null values will be drawn beyond the horizontal null value separator!
+ }
+ if (counts[p[k]] === undefined) {
+ counts[p[k]] = 1;
+ } else {
+ counts[p[k]] = counts[p[k]] + 1;
+ }
+ });
+
+ domain = Object.getOwnPropertyNames(counts).sort(function(a, b) {
+ return counts[a] - counts[b];
+ });
+
+ return d3.scale.ordinal()
+ .domain(domain)
+ .rangePoints(getRange());
+ }
+ };
+
+ __.dimensions.forEach(function(k) {
+ yscale[k] = defaultScales[__.types[k]](k);
+ });
+
+ __.hideAxis.forEach(function(k) {
+ yscale[k] = defaultScales[__.types[k]](k);
+ });
+
+ // xscale
+ xscale.rangePoints([0, w()], 1);
+
+ // canvas sizes
+ pc.selection.selectAll("canvas")
+ .style("margin-top", __.margin.top + "px")
+ .style("margin-left", __.margin.left + "px")
+ .attr("width", w()+2)
+ .attr("height", h()+2);
+
+ // default styles, needs to be set when canvas width changes
+ ctx.foreground.strokeStyle = __.color;
+ ctx.foreground.lineWidth = 1.4;
+ ctx.foreground.globalCompositeOperation = __.composite;
+ ctx.foreground.globalAlpha = __.alpha;
+ ctx.brushed.strokeStyle = __.brushedColor;
+ ctx.brushed.lineWidth = 1.4;
+ ctx.brushed.globalCompositeOperation = __.composite;
+ ctx.brushed.globalAlpha = __.alpha;
+ ctx.highlight.lineWidth = 3;
+
+ return this;
+ };
+
+ pc.scale = function(d, domain) {
+ yscale[d].domain(domain);
+
+ return this;
+ };
+
+ pc.flip = function(d) {
+ //yscale[d].domain().reverse(); // does not work
+ yscale[d].domain(yscale[d].domain().reverse()); // works
+
+ return this;
+ };
+
+ pc.commonScale = function(global, type) {
+ var t = type || "number";
+ if (typeof global === 'undefined') {
+ global = true;
+ }
+
+ // scales of the same type
+ var scales = __.dimensions.concat(__.hideAxis).filter(function(p) {
+ return __.types[p] == t;
+ });
+
+ if (global) {
+ var extent = d3.extent(scales.map(function(p,i) {
+ return yscale[p].domain();
+ }).reduce(function(a,b) {
+ return a.concat(b);
+ }));
+
+ scales.forEach(function(d) {
+ yscale[d].domain(extent);
+ });
+
+ } else {
+ scales.forEach(function(k) {
+ yscale[k].domain(d3.extent(__.data, function(d) { return +d[k]; }));
+ });
+ }
+
+ // update centroids
+ if (__.bundleDimension !== null) {
+ pc.bundleDimension(__.bundleDimension);
+ }
+
+ return this;
+ };
+ pc.detectDimensions = function() {
+ pc.types(pc.detectDimensionTypes(__.data));
+ pc.dimensions(d3.keys(pc.types()));
+ return this;
+ };
+
+ // a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
+ pc.toType = function(v) {
+ return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
+ };
+
+ // try to coerce to number before returning type
+ pc.toTypeCoerceNumbers = function(v) {
+ if ((parseFloat(v) == v) && (v != null)) {
+ return "number";
+ }
+ return pc.toType(v);
+ };
+
+ // attempt to determine types of each dimension based on first row of data
+ pc.detectDimensionTypes = function(data) {
+ var types = {};
+ d3.keys(data[0])
+ .forEach(function(col) {
+ types[col] = pc.toTypeCoerceNumbers(data[0][col]);
+ });
+ return types;
+ };
+ pc.render = function() {
+ // try to autodetect dimensions and create scales
+ if (!__.dimensions.length) pc.detectDimensions();
+ if (!(__.dimensions[0] in yscale)) pc.autoscale();
+
+ pc.render[__.mode]();
+
+ events.render.call(this);
+ return this;
+ };
+
+ pc.renderBrushed = function() {
+ if (!__.dimensions.length) pc.detectDimensions();
+ if (!(__.dimensions[0] in yscale)) pc.autoscale();
+
+ pc.renderBrushed[__.mode]();
+
+ events.render.call(this);
+ return this;
+ };
+
+ function isBrushed() {
+ if (__.brushed && __.brushed.length !== __.data.length)
+ return true;
+
+ var object = brush.currentMode().brushState();
+
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ pc.render.default = function() {
+ pc.clear('foreground');
+ pc.clear('highlight');
+
+ pc.renderBrushed.default();
+
+ __.data.forEach(path_foreground);
+ };
+
+ var foregroundQueue = d3.renderQueue(path_foreground)
+ .rate(50)
+ .clear(function() {
+ pc.clear('foreground');
+ pc.clear('highlight');
+ });
+
+ pc.render.queue = function() {
+ pc.renderBrushed.queue();
+
+ foregroundQueue(__.data);
+ };
+
+ pc.renderBrushed.default = function() {
+ pc.clear('brushed');
+
+ if (isBrushed()) {
+ __.brushed.forEach(path_brushed);
+ }
+ };
+
+ var brushedQueue = d3.renderQueue(path_brushed)
+ .rate(50)
+ .clear(function() {
+ pc.clear('brushed');
+ });
+
+ pc.renderBrushed.queue = function() {
+ if (isBrushed()) {
+ brushedQueue(__.brushed);
+ } else {
+ brushedQueue([]); // This is needed to clear the currently brushed items
+ }
+ };
+ function compute_cluster_centroids(d) {
+
+ var clusterCentroids = d3.map();
+ var clusterCounts = d3.map();
+ // determine clusterCounts
+ __.data.forEach(function(row) {
+ var scaled = yscale[d](row[d]);
+ if (!clusterCounts.has(scaled)) {
+ clusterCounts.set(scaled, 0);
+ }
+ var count = clusterCounts.get(scaled);
+ clusterCounts.set(scaled, count + 1);
+ });
+
+ __.data.forEach(function(row) {
+ __.dimensions.map(function(p, i) {
+ var scaled = yscale[d](row[d]);
+ if (!clusterCentroids.has(scaled)) {
+ var map = d3.map();
+ clusterCentroids.set(scaled, map);
+ }
+ if (!clusterCentroids.get(scaled).has(p)) {
+ clusterCentroids.get(scaled).set(p, 0);
+ }
+ var value = clusterCentroids.get(scaled).get(p);
+ value += yscale[p](row[p]) / clusterCounts.get(scaled);
+ clusterCentroids.get(scaled).set(p, value);
+ });
+ });
+
+ return clusterCentroids;
+
+ }
+
+ function compute_centroids(row) {
+ var centroids = [];
+
+ var p = __.dimensions;
+ var cols = p.length;
+ var a = 0.5; // center between axes
+ for (var i = 0; i < cols; ++i) {
+ // centroids on 'real' axes
+ var x = position(p[i]);
+ var y = yscale[p[i]](row[p[i]]);
+ centroids.push($V([x, y]));
+
+ // centroids on 'virtual' axes
+ if (i < cols - 1) {
+ var cx = x + a * (position(p[i+1]) - x);
+ var cy = y + a * (yscale[p[i+1]](row[p[i+1]]) - y);
+ if (__.bundleDimension !== null) {
+ var leftCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i]);
+ var rightCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i+1]);
+ var centroid = 0.5 * (leftCentroid + rightCentroid);
+ cy = centroid + (1 - __.bundlingStrength) * (cy - centroid);
+ }
+ centroids.push($V([cx, cy]));
+ }
+ }
+
+ return centroids;
+ }
+
+ function compute_control_points(centroids) {
+
+ var cols = centroids.length;
+ var a = __.smoothness;
+ var cps = [];
+
+ cps.push(centroids[0]);
+ cps.push($V([centroids[0].e(1) + a*2*(centroids[1].e(1)-centroids[0].e(1)), centroids[0].e(2)]));
+ for (var col = 1; col < cols - 1; ++col) {
+ var mid = centroids[col];
+ var left = centroids[col - 1];
+ var right = centroids[col + 1];
+
+ var diff = left.subtract(right);
+ cps.push(mid.add(diff.x(a)));
+ cps.push(mid);
+ cps.push(mid.subtract(diff.x(a)));
+ }
+ cps.push($V([centroids[cols-1].e(1) + a*2*(centroids[cols-2].e(1)-centroids[cols-1].e(1)), centroids[cols-1].e(2)]));
+ cps.push(centroids[cols - 1]);
+
+ return cps;
+
+ };
+
+ pc.shadows = function() {
+ flags.shadows = true;
+ pc.alphaOnBrushed(0.1);
+ pc.render();
+ return this;
+ };
+
+ // draw dots with radius r on the axis line where data intersects
+ pc.axisDots = function(r) {
+ var r = r || 0.1;
+ var ctx = pc.ctx.marks;
+ var startAngle = 0;
+ var endAngle = 2 * Math.PI;
+ ctx.globalAlpha = d3.min([ 1 / Math.pow(__.data.length, 1 / 2), 1 ]);
+ __.data.forEach(function(d) {
+ __.dimensions.map(function(p, i) {
+ ctx.beginPath();
+ ctx.arc(position(p), yscale[p](d[p]), r, startAngle, endAngle);
+ ctx.stroke();
+ ctx.fill();
+ });
+ });
+ return this;
+ };
+
+ // draw single cubic bezier curve
+ function single_curve(d, ctx) {
+
+ var centroids = compute_centroids(d);
+ var cps = compute_control_points(centroids);
+
+ ctx.moveTo(cps[0].e(1), cps[0].e(2));
+ for (var i = 1; i < cps.length; i += 3) {
+ if (__.showControlPoints) {
+ for (var j = 0; j < 3; j++) {
+ ctx.fillRect(cps[i+j].e(1), cps[i+j].e(2), 2, 2);
+ }
+ }
+ ctx.bezierCurveTo(cps[i].e(1), cps[i].e(2), cps[i+1].e(1), cps[i+1].e(2), cps[i+2].e(1), cps[i+2].e(2));
+ }
+ };
+
+ // draw single polyline
+ function color_path(d, ctx) {
+ ctx.beginPath();
+ if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
+ single_curve(d, ctx);
+ } else {
+ single_path(d, ctx);
+ }
+ ctx.stroke();
+ };
+
+ // draw many polylines of the same color
+ function paths(data, ctx) {
+ ctx.clearRect(-1, -1, w() + 2, h() + 2);
+ ctx.beginPath();
+ data.forEach(function(d) {
+ if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
+ single_curve(d, ctx);
+ } else {
+ single_path(d, ctx);
+ }
+ });
+ ctx.stroke();
+ };
+
+ // returns the y-position just beyond the separating null value line
+ function getNullPosition() {
+ if (__.nullValueSeparator=="bottom") {
+ return h()+1;
+ } else if (__.nullValueSeparator=="top") {
+ return 1;
+ } else {
+ console.log("A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'.");
+ }
+ return h()+1;
+ };
+
+ function single_path(d, ctx) {
+ __.dimensions.map(function(p, i) {
+ if (i == 0) {
+ ctx.moveTo(position(p), typeof d[p] =='undefined' ? getNullPosition() : yscale[p](d[p]));
+ } else {
+ ctx.lineTo(position(p), typeof d[p] =='undefined' ? getNullPosition() : yscale[p](d[p]));
+ }
+ });
+ };
+
+ function path_brushed(d, i) {
+ if (__.brushedColor !== null) {
+ ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i);
+ } else {
+ ctx.brushed.strokeStyle = d3.functor(__.color)(d, i);
+ }
+ return color_path(d, ctx.brushed)
+ };
+
+ function path_foreground(d, i) {
+ ctx.foreground.strokeStyle = d3.functor(__.color)(d, i);
+ return color_path(d, ctx.foreground);
+ };
+
+ function path_highlight(d, i) {
+ ctx.highlight.strokeStyle = d3.functor(__.color)(d, i);
+ return color_path(d, ctx.highlight);
+ };
+ pc.clear = function(layer) {
+ ctx[layer].clearRect(0, 0, w() + 2, h() + 2);
+
+ // This will make sure that the foreground items are transparent
+ // without the need for changing the opacity style of the foreground canvas
+ // as this would stop the css styling from working
+ if(layer === "brushed" && isBrushed()) {
+ ctx.brushed.fillStyle = pc.selection.style("background-color");
+ ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed;
+ ctx.brushed.fillRect(0, 0, w() + 2, h() + 2);
+ ctx.brushed.globalAlpha = __.alpha;
+ }
+ return this;
+ };
+
+ d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat");
+
+ function flipAxisAndUpdatePCP(dimension) {
+ var g = pc.svg.selectAll(".dimension");
+
+ pc.flip(dimension);
+
+ d3.select(this.parentElement)
+ .transition()
+ .duration(1100)
+ .call(axis.scale(yscale[dimension]));
+
+ pc.render();
+ }
+
+ function rotateLabels() {
+ var delta = d3.event.deltaY;
+ delta = delta < 0 ? -5 : delta;
+ delta = delta > 0 ? 5 : delta;
+
+ __.dimensionTitleRotation += delta;
+ pc.svg.selectAll("text.label")
+ .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
+ d3.event.preventDefault();
+ }
+
+ function dimensionLabels(d) {
+ return d in __.dimensionTitles ? __.dimensionTitles[d] : d; // dimension display names
+ }
+
+ pc.createAxes = function() {
+ if (g) pc.removeAxes();
+
+ // Add a group element for each dimension.
+ g = pc.svg.selectAll(".dimension")
+ .data(__.dimensions, function(d) { return d; })
+ .enter().append("svg:g")
+ .attr("class", "dimension")
+ .attr("transform", function(d) { return "translate(" + xscale(d) + ")"; });
+
+ // Add an axis and title.
+ g.append("svg:g")
+ .attr("class", "axis")
+ .attr("transform", "translate(0,0)")
+ .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
+ .append("svg:text")
+ .attr({
+ "text-anchor": "middle",
+ "y": 0,
+ "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
+ "x": 0,
+ "class": "label"
+ })
+ .text(dimensionLabels)
+ .on("dblclick", flipAxisAndUpdatePCP)
+ .on("wheel", rotateLabels);
+
+ if (__.nullValueSeparator=="top") {
+ pc.svg.append("line")
+ .attr("x1", 0)
+ .attr("y1", 1+__.nullValueSeparatorPadding.top)
+ .attr("x2", w())
+ .attr("y2", 1+__.nullValueSeparatorPadding.top)
+ .attr("stroke-width", 1)
+ .attr("stroke", "#777")
+ .attr("fill", "none")
+ .attr("shape-rendering", "crispEdges");
+ } else if (__.nullValueSeparator=="bottom") {
+ pc.svg.append("line")
+ .attr("x1", 0)
+ .attr("y1", h()+1-__.nullValueSeparatorPadding.bottom)
+ .attr("x2", w())
+ .attr("y2", h()+1-__.nullValueSeparatorPadding.bottom)
+ .attr("stroke-width", 1)
+ .attr("stroke", "#777")
+ .attr("fill", "none")
+ .attr("shape-rendering", "crispEdges");
+ }
+
+ flags.axes= true;
+ return this;
+ };
+
+ pc.removeAxes = function() {
+ g.remove();
+ return this;
+ };
+
+ pc.updateAxes = function() {
+ var g_data = pc.svg.selectAll(".dimension").data(__.dimensions);
+
+ // Enter
+ g_data.enter().append("svg:g")
+ .attr("class", "dimension")
+ .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
+ .style("opacity", 0)
+ .append("svg:g")
+ .attr("class", "axis")
+ .attr("transform", "translate(0,0)")
+ .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); })
+ .append("svg:text")
+ .attr({
+ "text-anchor": "middle",
+ "y": 0,
+ "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")",
+ "x": 0,
+ "class": "label"
+ })
+ .text(dimensionLabels)
+ .on("dblclick", flipAxisAndUpdatePCP)
+ .on("wheel", rotateLabels);
+
+ // Update
+ g_data.attr("opacity", 0);
+ g_data.select(".axis")
+ .transition()
+ .duration(1100)
+ .each(function(d) {
+ d3.select(this).call(axis.scale(yscale[d]));
+ });
+ g_data.select(".label")
+ .transition()
+ .duration(1100)
+ .text(dimensionLabels)
+ .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")");
+
+ // Exit
+ g_data.exit().remove();
+
+ g = pc.svg.selectAll(".dimension");
+ g.transition().duration(1100)
+ .attr("transform", function(p) { return "translate(" + position(p) + ")"; })
+ .style("opacity", 1);
+
+ pc.svg.selectAll(".axis")
+ .transition()
+ .duration(1100)
+ .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); });
+
+ if (flags.brushable) pc.brushable();
+ if (flags.reorderable) pc.reorderable();
+ if (pc.brushMode() !== "None") {
+ var mode = pc.brushMode();
+ pc.brushMode("None");
+ pc.brushMode(mode);
+ }
+ return this;
+ };
+
+ // Jason Davies, http://bl.ocks.org/1341281
+ pc.reorderable = function() {
+ if (!g) pc.createAxes();
+
+ g.style("cursor", "move")
+ .call(d3.behavior.drag()
+ .on("dragstart", function(d) {
+ dragging[d] = this.__origin__ = xscale(d);
+ })
+ .on("drag", function(d) {
+ dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx));
+ __.dimensions.sort(function(a, b) { return position(a) - position(b); });
+ xscale.domain(__.dimensions);
+ pc.render();
+ g.attr("transform", function(d) { return "translate(" + position(d) + ")"; });
+ })
+ .on("dragend", function(d) {
+ // Let's see if the order has changed and send out an event if so.
+ var i = 0,
+ j = __.dimensions.indexOf(d),
+ elem = this,
+ parent = this.parentElement;
+
+ while((elem = elem.previousElementSibling) != null) ++i;
+ if (i !== j) {
+ events.axesreorder.call(pc, __.dimensions);
+ // We now also want to reorder the actual dom elements that represent
+ // the axes. That is, the g.dimension elements. If we don't do this,
+ // we get a weird and confusing transition when updateAxes is called.
+ // This is due to the fact that, initially the nth g.dimension element
+ // represents the nth axis. However, after a manual reordering,
+ // without reordering the dom elements, the nth dom elements no longer
+ // necessarily represents the nth axis.
+ //
+ // i is the original index of the dom element
+ // j is the new index of the dom element
+ if (i > j) { // Element moved left
+ parent.insertBefore(this, parent.children[j - 1]);
+ } else { // Element moved right
+ if ((j + 1) < parent.children.length) {
+ parent.insertBefore(this, parent.children[j + 1]);
+ } else {
+ parent.appendChild(this);
+ }
+ }
+ }
+
+ delete this.__origin__;
+ delete dragging[d];
+ d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")");
+ pc.render();
+ }));
+ flags.reorderable = true;
+ return this;
+ };
+
+ // Reorder dimensions, such that the highest value (visually) is on the left and
+ // the lowest on the right. Visual values are determined by the data values in
+ // the given row.
+ pc.reorder = function(rowdata) {
+ var dims = __.dimensions.slice(0);
+ __.dimensions.sort(function(a, b) {
+ var pixelDifference = yscale[a](rowdata[a]) - yscale[b](rowdata[b]);
+
+ // Array.sort is not necessarily stable, this means that if pixelDifference is zero
+ // the ordering of dimensions might change unexpectedly. This is solved by sorting on
+ // variable name in that case.
+ if (pixelDifference === 0) {
+ return a.localeCompare(b);
+ } // else
+ return pixelDifference;
+ });
+
+ // NOTE: this is relatively cheap given that:
+ // number of dimensions < number of data items
+ // Thus we check equality of order to prevent rerendering when this is the case.
+ var reordered = false;
+ dims.some(function(val, index) {
+ reordered = val !== __.dimensions[index];
+ return reordered;
+ });
+
+ if (reordered) {
+ xscale.domain(__.dimensions);
+ var highlighted = __.highlighted.slice(0);
+ pc.unhighlight();
+
+ g.transition()
+ .duration(1500)
+ .attr("transform", function(d) {
+ return "translate(" + xscale(d) + ")";
+ });
+ pc.render();
+
+ // pc.highlight() does not check whether highlighted is length zero, so we do that here.
+ if (highlighted.length !== 0) {
+ pc.highlight(highlighted);
+ }
+ }
+ }
+
+ // pairs of adjacent dimensions
+ pc.adjacent_pairs = function(arr) {
+ var ret = [];
+ for (var i = 0; i < arr.length-1; i++) {
+ ret.push([arr[i],arr[i+1]]);
+ };
+ return ret;
+ };
+
+ var brush = {
+ modes: {
+ "None": {
+ install: function(pc) {}, // Nothing to be done.
+ uninstall: function(pc) {}, // Nothing to be done.
+ selected: function() { return []; }, // Nothing to return
+ brushState: function() { return {}; }
+ }
+ },
+ mode: "None",
+ predicate: "AND",
+ currentMode: function() {
+ return this.modes[this.mode];
+ }
+ };
+
+ // This function can be used for 'live' updates of brushes. That is, during the
+ // specification of a brush, this method can be called to update the view.
+ //
+ // @param newSelection - The new set of data items that is currently contained
+ // by the brushes
+ function brushUpdated(newSelection) {
+ __.brushed = newSelection;
+ events.brush.call(pc,__.brushed);
+ pc.renderBrushed();
+ }
+
+ function brushPredicate(predicate) {
+ if (!arguments.length) { return brush.predicate; }
+
+ predicate = String(predicate).toUpperCase();
+ if (predicate !== "AND" && predicate !== "OR") {
+ throw "Invalid predicate " + predicate;
+ }
+
+ brush.predicate = predicate;
+ __.brushed = brush.currentMode().selected();
+ pc.renderBrushed();
+ return pc;
+ }
+
+ pc.brushModes = function() {
+ return Object.getOwnPropertyNames(brush.modes);
+ };
+
+ pc.brushMode = function(mode) {
+ if (arguments.length === 0) {
+ return brush.mode;
+ }
+
+ if (pc.brushModes().indexOf(mode) === -1) {
+ throw "pc.brushmode: Unsupported brush mode: " + mode;
+ }
+
+ // Make sure that we don't trigger unnecessary events by checking if the mode
+ // actually changes.
+ if (mode !== brush.mode) {
+ // When changing brush modes, the first thing we need to do is clearing any
+ // brushes from the current mode, if any.
+ if (brush.mode !== "None") {
+ pc.brushReset();
+ }
+
+ // Next, we need to 'uninstall' the current brushMode.
+ brush.modes[brush.mode].uninstall(pc);
+ // Finally, we can install the requested one.
+ brush.mode = mode;
+ brush.modes[brush.mode].install();
+ if (mode === "None") {
+ delete pc.brushPredicate;
+ } else {
+ pc.brushPredicate = brushPredicate;
+ }
+ }
+
+ return pc;
+ };
+
+ // brush mode: 1D-Axes
+
+ (function() {
+ var brushes = {};
+
+ function is_brushed(p) {
+ return !brushes[p].empty();
+ }
+
+ // data within extents
+ function selected() {
+ var actives = __.dimensions.filter(is_brushed),
+ extents = actives.map(function(p) { return brushes[p].extent(); });
+
+ // We don't want to return the full data set when there are no axes brushed.
+ // Actually, when there are no axes brushed, by definition, no items are
+ // selected. So, let's avoid the filtering and just return false.
+ //if (actives.length === 0) return false;
+
+ // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
+ if (actives.length === 0) return __.data;
+
+ // test if within range
+ var within = {
+ "date": function(d,p,dimension) {
+ if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+ return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
+ } else {
+ return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
+ }
+ },
+ "number": function(d,p,dimension) {
+ if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+ return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
+ } else {
+ return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1]
+ }
+ },
+ "string": function(d,p,dimension) {
+ return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
+ }
+ };
+
+ return __.data
+ .filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return actives.every(function(p, dimension) {
+ return within[__.types[p]](d,p,dimension);
+ });
+ case "OR":
+ return actives.some(function(p, dimension) {
+ return within[__.types[p]](d,p,dimension);
+ });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ };
+
+ function brushExtents(extents) {
+ if(typeof(extents) === 'undefined')
+ {
+ var extents = {};
+ __.dimensions.forEach(function(d) {
+ var brush = brushes[d];
+ if (brush !== undefined && !brush.empty()) {
+ var extent = brush.extent();
+ extent.sort(d3.ascending);
+ extents[d] = extent;
+ }
+ });
+ return extents;
+ }
+ else
+ {
+ //first get all the brush selections
+ var brushSelections = {};
+ g.selectAll('.brush')
+ .each(function(d) {
+ brushSelections[d] = d3.select(this);
+
+ });
+
+ // loop over each dimension and update appropriately (if it was passed in through extents)
+ __.dimensions.forEach(function(d) {
+ if (extents[d] === undefined){
+ return;
+ }
+
+ var brush = brushes[d];
+ if (brush !== undefined) {
+ //update the extent
+ brush.extent(extents[d]);
+
+ //redraw the brush
+ brush(brushSelections[d]);
+
+ //fire some events
+ brush.event(brushSelections[d]);
+ }
+ });
+
+ //redraw the chart
+ pc.renderBrushed();
+ }
+ }
+ function brushFor(axis) {
+ var brush = d3.svg.brush();
+
+ brush
+ .y(yscale[axis])
+ .on("brushstart", function() {
+ if(d3.event.sourceEvent !== null) {
+ d3.event.sourceEvent.stopPropagation();
+ }
+ })
+ .on("brush", function() {
+ brushUpdated(selected());
+ })
+ .on("brushend", function() {
+ events.brushend.call(pc, __.brushed);
+ });
+
+ brushes[axis] = brush;
+ return brush;
+ };
+ function brushReset(dimension) {
+ __.brushed = false;
+ if (g) {
+ g.selectAll('.brush')
+ .each(function(d) {
+ d3.select(this).call(
+ brushes[d].clear()
+ );
+ });
+ pc.renderBrushed();
+ }
+ return this;
+ };
+
+ function install() {
+ if (!g) pc.createAxes();
+
+ // Add and store a brush for each axis.
+ g.append("svg:g")
+ .attr("class", "brush")
+ .each(function(d) {
+ d3.select(this).call(brushFor(d));
+ })
+ .selectAll("rect")
+ .style("visibility", null)
+ .attr("x", -15)
+ .attr("width", 30);
+
+ pc.brushExtents = brushExtents;
+ pc.brushReset = brushReset;
+ return pc;
+ };
+
+ brush.modes["1D-axes"] = {
+ install: install,
+ uninstall: function() {
+ g.selectAll(".brush").remove();
+ brushes = {};
+ delete pc.brushExtents;
+ delete pc.brushReset;
+ },
+ selected: selected,
+ brushState: brushExtents
+ }
+ })();
+ // brush mode: 2D-strums
+ // bl.ocks.org/syntagmatic/5441022
+
+ (function() {
+ var strums = {},
+ strumRect;
+
+ function drawStrum(strum, activePoint) {
+ var svg = pc.selection.select("svg").select("g#strums"),
+ id = strum.dims.i,
+ points = [strum.p1, strum.p2],
+ line = svg.selectAll("line#strum-" + id).data([strum]),
+ circles = svg.selectAll("circle#strum-" + id).data(points),
+ drag = d3.behavior.drag();
+
+ line.enter()
+ .append("line")
+ .attr("id", "strum-" + id)
+ .attr("class", "strum");
+
+ line
+ .attr("x1", function(d) { return d.p1[0]; })
+ .attr("y1", function(d) { return d.p1[1]; })
+ .attr("x2", function(d) { return d.p2[0]; })
+ .attr("y2", function(d) { return d.p2[1]; })
+ .attr("stroke", "black")
+ .attr("stroke-width", 2);
+
+ drag
+ .on("drag", function(d, i) {
+ var ev = d3.event;
+ i = i + 1;
+ strum["p" + i][0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX);
+ strum["p" + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY);
+ drawStrum(strum, i - 1);
+ })
+ .on("dragend", onDragEnd());
+
+ circles.enter()
+ .append("circle")
+ .attr("id", "strum-" + id)
+ .attr("class", "strum");
+
+ circles
+ .attr("cx", function(d) { return d[0]; })
+ .attr("cy", function(d) { return d[1]; })
+ .attr("r", 5)
+ .style("opacity", function(d, i) {
+ return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
+ })
+ .on("mouseover", function() {
+ d3.select(this).style("opacity", 0.8);
+ })
+ .on("mouseout", function() {
+ d3.select(this).style("opacity", 0);
+ })
+ .call(drag);
+ }
+
+ function dimensionsForPoint(p) {
+ var dims = { i: -1, left: undefined, right: undefined };
+ __.dimensions.some(function(dim, i) {
+ if (xscale(dim) < p[0]) {
+ var next = __.dimensions[i + 1];
+ dims.i = i;
+ dims.left = dim;
+ dims.right = next;
+ return false;
+ }
+ return true;
+ });
+
+ if (dims.left === undefined) {
+ // Event on the left side of the first axis.
+ dims.i = 0;
+ dims.left = __.dimensions[0];
+ dims.right = __.dimensions[1];
+ } else if (dims.right === undefined) {
+ // Event on the right side of the last axis
+ dims.i = __.dimensions.length - 1;
+ dims.right = dims.left;
+ dims.left = __.dimensions[__.dimensions.length - 2];
+ }
+
+ return dims;
+ }
+
+ function onDragStart() {
+ // First we need to determine between which two axes the sturm was started.
+ // This will determine the freedom of movement, because a strum can
+ // logically only happen between two axes, so no movement outside these axes
+ // should be allowed.
+ return function() {
+ var p = d3.mouse(strumRect[0][0]),
+ dims,
+ strum;
+
+ p[0] = p[0] - __.margin.left;
+ p[1] = p[1] - __.margin.top;
+
+ dims = dimensionsForPoint(p),
+ strum = {
+ p1: p,
+ dims: dims,
+ minX: xscale(dims.left),
+ maxX: xscale(dims.right),
+ minY: 0,
+ maxY: h()
+ };
+
+ strums[dims.i] = strum;
+ strums.active = dims.i;
+
+ // Make sure that the point is within the bounds
+ strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX);
+ strum.p2 = strum.p1.slice();
+ };
+ }
+
+ function onDrag() {
+ return function() {
+ var ev = d3.event,
+ strum = strums[strums.active];
+
+ // Make sure that the point is within the bounds
+ strum.p2[0] = Math.min(Math.max(strum.minX + 1, ev.x - __.margin.left), strum.maxX);
+ strum.p2[1] = Math.min(Math.max(strum.minY, ev.y - __.margin.top), strum.maxY);
+ drawStrum(strum, 1);
+ };
+ }
+
+ function containmentTest(strum, width) {
+ var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX],
+ p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX],
+ m1 = 1 - width / p1[0],
+ b1 = p1[1] * (1 - m1),
+ m2 = 1 - width / p2[0],
+ b2 = p2[1] * (1 - m2);
+
+ // test if point falls between lines
+ return function(p) {
+ var x = p[0],
+ y = p[1],
+ y1 = m1 * x + b1,
+ y2 = m2 * x + b2;
+
+ if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) {
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ function selected() {
+ var ids = Object.getOwnPropertyNames(strums),
+ brushed = __.data;
+
+ // Get the ids of the currently active strums.
+ ids = ids.filter(function(d) {
+ return !isNaN(d);
+ });
+
+ function crossesStrum(d, id) {
+ var strum = strums[id],
+ test = containmentTest(strum, strums.width(id)),
+ d1 = strum.dims.left,
+ d2 = strum.dims.right,
+ y1 = yscale[d1],
+ y2 = yscale[d2],
+ point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX];
+ return test(point);
+ }
+
+ if (ids.length === 0) { return brushed; }
+
+ return brushed.filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return ids.every(function(id) { return crossesStrum(d, id); });
+ case "OR":
+ return ids.some(function(id) { return crossesStrum(d, id); });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ }
+
+ function removeStrum() {
+ var strum = strums[strums.active],
+ svg = pc.selection.select("svg").select("g#strums");
+
+ delete strums[strums.active];
+ strums.active = undefined;
+ svg.selectAll("line#strum-" + strum.dims.i).remove();
+ svg.selectAll("circle#strum-" + strum.dims.i).remove();
+ }
+
+ function onDragEnd() {
+ return function() {
+ var brushed = __.data,
+ strum = strums[strums.active];
+
+ // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
+ // considered a drag without move. So we have to deal with that case
+ if (strum && strum.p1[0] === strum.p2[0] && strum.p1[1] === strum.p2[1]) {
+ removeStrum(strums);
+ }
+
+ brushed = selected(strums);
+ strums.active = undefined;
+ __.brushed = brushed;
+ pc.renderBrushed();
+ events.brushend.call(pc, __.brushed);
+ };
+ }
+
+ function brushReset(strums) {
+ return function() {
+ var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ ids.forEach(function(d) {
+ strums.active = d;
+ removeStrum(strums);
+ });
+ onDragEnd(strums)();
+ };
+ }
+
+ function install() {
+ var drag = d3.behavior.drag();
+
+ // Map of current strums. Strums are stored per segment of the PC. A segment,
+ // being the area between two axes. The left most area is indexed at 0.
+ strums.active = undefined;
+ // Returns the width of the PC segment where currently a strum is being
+ // placed. NOTE: even though they are evenly spaced in our current
+ // implementation, we keep for when non-even spaced segments are supported as
+ // well.
+ strums.width = function(id) {
+ var strum = strums[id];
+
+ if (strum === undefined) {
+ return undefined;
+ }
+
+ return strum.maxX - strum.minX;
+ };
+
+ pc.on("axesreorder.strums", function() {
+ var ids = Object.getOwnPropertyNames(strums).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ // Checks if the first dimension is directly left of the second dimension.
+ function consecutive(first, second) {
+ var length = __.dimensions.length;
+ return __.dimensions.some(function(d, i) {
+ return (d === first)
+ ? i + i < length && __.dimensions[i + 1] === second
+ : false;
+ });
+ }
+
+ if (ids.length > 0) { // We have some strums, which might need to be removed.
+ ids.forEach(function(d) {
+ var dims = strums[d].dims;
+ strums.active = d;
+ // If the two dimensions of the current strum are not next to each other
+ // any more, than we'll need to remove the strum. Otherwise we keep it.
+ if (!consecutive(dims.left, dims.right)) {
+ removeStrum(strums);
+ }
+ });
+ onDragEnd(strums)();
+ }
+ });
+
+ // Add a new svg group in which we draw the strums.
+ pc.selection.select("svg").append("g")
+ .attr("id", "strums")
+ .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ // Install the required brushReset function
+ pc.brushReset = brushReset(strums);
+
+ drag
+ .on("dragstart", onDragStart(strums))
+ .on("drag", onDrag(strums))
+ .on("dragend", onDragEnd(strums));
+
+ // NOTE: The styling needs to be done here and not in the css. This is because
+ // for 1D brushing, the canvas layers should not listen to
+ // pointer-events.
+ strumRect = pc.selection.select("svg").insert("rect", "g#strums")
+ .attr("id", "strum-events")
+ .attr("x", __.margin.left)
+ .attr("y", __.margin.top)
+ .attr("width", w())
+ .attr("height", h() + 2)
+ .style("opacity", 0)
+ .call(drag);
+ }
+
+ brush.modes["2D-strums"] = {
+ install: install,
+ uninstall: function() {
+ pc.selection.select("svg").select("g#strums").remove();
+ pc.selection.select("svg").select("rect#strum-events").remove();
+ pc.on("axesreorder.strums", undefined);
+ delete pc.brushReset;
+
+ strumRect = undefined;
+ },
+ selected: selected,
+ brushState: function () { return strums; }
+ };
+
+ }());
+
+ // brush mode: 1D-Axes with multiple extents
+ // requires d3.svg.multibrush
+
+ (function() {
+ if (typeof d3.svg.multibrush !== 'function') {
+ return;
+ }
+ var brushes = {};
+
+ function is_brushed(p) {
+ return !brushes[p].empty();
+ }
+
+ // data within extents
+ function selected() {
+ var actives = __.dimensions.filter(is_brushed),
+ extents = actives.map(function(p) { return brushes[p].extent(); });
+
+ // We don't want to return the full data set when there are no axes brushed.
+ // Actually, when there are no axes brushed, by definition, no items are
+ // selected. So, let's avoid the filtering and just return false.
+ //if (actives.length === 0) return false;
+
+ // Resolves broken examples for now. They expect to get the full dataset back from empty brushes
+ if (actives.length === 0) return __.data;
+
+ // test if within range
+ var within = {
+ "date": function(d,p,dimension,b) {
+ if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+ return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1]
+ } else {
+ return b[0] <= d[p] && d[p] <= b[1]
+ }
+ },
+ "number": function(d,p,dimension,b) {
+ if (typeof yscale[p].rangePoints === "function") { // if it is ordinal
+ return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1]
+ } else {
+ return b[0] <= d[p] && d[p] <= b[1]
+ }
+ },
+ "string": function(d,p,dimension,b) {
+ return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1]
+ }
+ };
+
+ return __.data
+ .filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return actives.every(function(p, dimension) {
+ return extents[dimension].some(function(b) {
+ return within[__.types[p]](d,p,dimension,b);
+ });
+ });
+ case "OR":
+ return actives.some(function(p, dimension) {
+ return extents[dimension].some(function(b) {
+ return within[__.types[p]](d,p,dimension,b);
+ });
+ });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ };
+
+ function brushExtents() {
+ var extents = {};
+ __.dimensions.forEach(function(d) {
+ var brush = brushes[d];
+ if (brush !== undefined && !brush.empty()) {
+ var extent = brush.extent();
+ extents[d] = extent;
+ }
+ });
+ return extents;
+ }
+
+ function brushFor(axis) {
+ var brush = d3.svg.multibrush();
+
+ brush
+ .y(yscale[axis])
+ .on("brushstart", function() {
+ if(d3.event.sourceEvent !== null) {
+ d3.event.sourceEvent.stopPropagation();
+ }
+ })
+ .on("brush", function() {
+ brushUpdated(selected());
+ })
+ .on("brushend", function() {
+ // d3.svg.multibrush clears extents just before calling 'brushend'
+ // so we have to update here again.
+ // This fixes issue #103 for now, but should be changed in d3.svg.multibrush
+ // to avoid unnecessary computation.
+ brushUpdated(selected());
+ events.brushend.call(pc, __.brushed);
+ })
+ .extentAdaption(function(selection) {
+ selection
+ .style("visibility", null)
+ .attr("x", -15)
+ .attr("width", 30);
+ })
+ .resizeAdaption(function(selection) {
+ selection
+ .selectAll("rect")
+ .attr("x", -15)
+ .attr("width", 30);
+ });
+
+ brushes[axis] = brush;
+ return brush;
+ }
+
+ function brushReset(dimension) {
+ __.brushed = false;
+ if (g) {
+ g.selectAll('.brush')
+ .each(function(d) {
+ d3.select(this).call(
+ brushes[d].clear()
+ );
+ });
+ pc.renderBrushed();
+ }
+ return this;
+ };
+
+ function install() {
+ if (!g) pc.createAxes();
+
+ // Add and store a brush for each axis.
+ g.append("svg:g")
+ .attr("class", "brush")
+ .each(function(d) {
+ d3.select(this).call(brushFor(d));
+ })
+ .selectAll("rect")
+ .style("visibility", null)
+ .attr("x", -15)
+ .attr("width", 30);
+
+ pc.brushExtents = brushExtents;
+ pc.brushReset = brushReset;
+ return pc;
+ }
+
+ brush.modes["1D-axes-multi"] = {
+ install: install,
+ uninstall: function() {
+ g.selectAll(".brush").remove();
+ brushes = {};
+ delete pc.brushExtents;
+ delete pc.brushReset;
+ },
+ selected: selected,
+ brushState: brushExtents
+ }
+ })();
+ // brush mode: angular
+ // code based on 2D.strums.js
+
+ (function() {
+ var arcs = {},
+ strumRect;
+
+ function drawStrum(arc, activePoint) {
+ var svg = pc.selection.select("svg").select("g#arcs"),
+ id = arc.dims.i,
+ points = [arc.p2, arc.p3],
+ line = svg.selectAll("line#arc-" + id).data([{p1:arc.p1,p2:arc.p2},{p1:arc.p1,p2:arc.p3}]),
+ circles = svg.selectAll("circle#arc-" + id).data(points),
+ drag = d3.behavior.drag(),
+ path = svg.selectAll("path#arc-" + id).data([arc]);
+
+ path.enter()
+ .append("path")
+ .attr("id", "arc-" + id)
+ .attr("class", "arc")
+ .style("fill", "orange")
+ .style("opacity", 0.5);
+
+ path
+ .attr("d", arc.arc)
+ .attr("transform", "translate(" + arc.p1[0] + "," + arc.p1[1] + ")");
+
+ line.enter()
+ .append("line")
+ .attr("id", "arc-" + id)
+ .attr("class", "arc");
+
+ line
+ .attr("x1", function(d) { return d.p1[0]; })
+ .attr("y1", function(d) { return d.p1[1]; })
+ .attr("x2", function(d) { return d.p2[0]; })
+ .attr("y2", function(d) { return d.p2[1]; })
+ .attr("stroke", "black")
+ .attr("stroke-width", 2);
+
+ drag
+ .on("drag", function(d, i) {
+ var ev = d3.event,
+ angle = 0;
+
+ i = i + 2;
+
+ arc["p" + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX);
+ arc["p" + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY);
+
+ angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id);
+
+ if ((arc.startAngle < Math.PI && arc.endAngle < Math.PI && angle < Math.PI) ||
+ (arc.startAngle >= Math.PI && arc.endAngle >= Math.PI && angle >= Math.PI)) {
+
+ if (i === 2) {
+ arc.endAngle = angle;
+ arc.arc.endAngle(angle);
+ } else if (i === 3) {
+ arc.startAngle = angle;
+ arc.arc.startAngle(angle);
+ }
+
+ }
+
+ drawStrum(arc, i - 2);
+ })
+ .on("dragend", onDragEnd());
+
+ circles.enter()
+ .append("circle")
+ .attr("id", "arc-" + id)
+ .attr("class", "arc");
+
+ circles
+ .attr("cx", function(d) { return d[0]; })
+ .attr("cy", function(d) { return d[1]; })
+ .attr("r", 5)
+ .style("opacity", function(d, i) {
+ return (activePoint !== undefined && i === activePoint) ? 0.8 : 0;
+ })
+ .on("mouseover", function() {
+ d3.select(this).style("opacity", 0.8);
+ })
+ .on("mouseout", function() {
+ d3.select(this).style("opacity", 0);
+ })
+ .call(drag);
+ }
+
+ function dimensionsForPoint(p) {
+ var dims = { i: -1, left: undefined, right: undefined };
+ __.dimensions.some(function(dim, i) {
+ if (xscale(dim) < p[0]) {
+ var next = __.dimensions[i + 1];
+ dims.i = i;
+ dims.left = dim;
+ dims.right = next;
+ return false;
+ }
+ return true;
+ });
+
+ if (dims.left === undefined) {
+ // Event on the left side of the first axis.
+ dims.i = 0;
+ dims.left = __.dimensions[0];
+ dims.right = __.dimensions[1];
+ } else if (dims.right === undefined) {
+ // Event on the right side of the last axis
+ dims.i = __.dimensions.length - 1;
+ dims.right = dims.left;
+ dims.left = __.dimensions[__.dimensions.length - 2];
+ }
+
+ return dims;
+ }
+
+ function onDragStart() {
+ // First we need to determine between which two axes the arc was started.
+ // This will determine the freedom of movement, because a arc can
+ // logically only happen between two axes, so no movement outside these axes
+ // should be allowed.
+ return function() {
+ var p = d3.mouse(strumRect[0][0]),
+ dims,
+ arc;
+
+ p[0] = p[0] - __.margin.left;
+ p[1] = p[1] - __.margin.top;
+
+ dims = dimensionsForPoint(p),
+ arc = {
+ p1: p,
+ dims: dims,
+ minX: xscale(dims.left),
+ maxX: xscale(dims.right),
+ minY: 0,
+ maxY: h(),
+ startAngle: undefined,
+ endAngle: undefined,
+ arc: d3.svg.arc().innerRadius(0)
+ };
+
+ arcs[dims.i] = arc;
+ arcs.active = dims.i;
+
+ // Make sure that the point is within the bounds
+ arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
+ arc.p2 = arc.p1.slice();
+ arc.p3 = arc.p1.slice();
+ };
+ }
+
+ function onDrag() {
+ return function() {
+ var ev = d3.event,
+ arc = arcs[arcs.active];
+
+ // Make sure that the point is within the bounds
+ arc.p2[0] = Math.min(Math.max(arc.minX + 1, ev.x - __.margin.left), arc.maxX);
+ arc.p2[1] = Math.min(Math.max(arc.minY, ev.y - __.margin.top), arc.maxY);
+ arc.p3 = arc.p2.slice();
+ // console.log(arcs.angle(arcs.active));
+ // console.log(signedAngle(arcs.unsignedAngle(arcs.active)));
+ drawStrum(arc, 1);
+ };
+ }
+
+ // some helper functions
+ function hypothenuse(a, b) {
+ return Math.sqrt(a*a + b*b);
+ }
+
+ var rad = (function() {
+ var c = Math.PI / 180;
+ return function(angle) {
+ return angle * c;
+ };
+ })();
+
+ var deg = (function() {
+ var c = 180 / Math.PI;
+ return function(angle) {
+ return angle * c;
+ };
+ })();
+
+ // [0, 2*PI] -> [-PI/2, PI/2]
+ var signedAngle = function(angle) {
+ var ret = angle;
+ if (angle > Math.PI) {
+ ret = angle - 1.5 * Math.PI;
+ ret = angle - 1.5 * Math.PI;
+ } else {
+ ret = angle - 0.5 * Math.PI;
+ ret = angle - 0.5 * Math.PI;
+ }
+ return -ret;
+ }
+
+ /**
+ * angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
+ * However, one can only select lines from 0 to PI, so we compute the
+ * 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
+ * are 12 and 6 o'clock respectively.
+ */
+ function containmentTest(arc) {
+ var startAngle = signedAngle(arc.startAngle);
+ var endAngle = signedAngle(arc.endAngle);
+
+ if (startAngle > endAngle) {
+ var tmp = startAngle;
+ startAngle = endAngle;
+ endAngle = tmp;
+ }
+
+ // test if segment angle is contained in angle interval
+ return function(a) {
+
+ if (a >= startAngle && a <= endAngle) {
+ return true;
+ }
+
+ return false;
+ };
+ }
+
+ function selected() {
+ var ids = Object.getOwnPropertyNames(arcs),
+ brushed = __.data;
+
+ // Get the ids of the currently active arcs.
+ ids = ids.filter(function(d) {
+ return !isNaN(d);
+ });
+
+ function crossesStrum(d, id) {
+ var arc = arcs[id],
+ test = containmentTest(arc),
+ d1 = arc.dims.left,
+ d2 = arc.dims.right,
+ y1 = yscale[d1],
+ y2 = yscale[d2],
+ a = arcs.width(id),
+ b = y1(d[d1]) - y2(d[d2]),
+ c = hypothenuse(a, b),
+ angle = Math.asin(b/c); // rad in [-PI/2, PI/2]
+ return test(angle);
+ }
+
+ if (ids.length === 0) { return brushed; }
+
+ return brushed.filter(function(d) {
+ switch(brush.predicate) {
+ case "AND":
+ return ids.every(function(id) { return crossesStrum(d, id); });
+ case "OR":
+ return ids.some(function(id) { return crossesStrum(d, id); });
+ default:
+ throw "Unknown brush predicate " + __.brushPredicate;
+ }
+ });
+ }
+
+ function removeStrum() {
+ var arc = arcs[arcs.active],
+ svg = pc.selection.select("svg").select("g#arcs");
+
+ delete arcs[arcs.active];
+ arcs.active = undefined;
+ svg.selectAll("line#arc-" + arc.dims.i).remove();
+ svg.selectAll("circle#arc-" + arc.dims.i).remove();
+ svg.selectAll("path#arc-" + arc.dims.i).remove();
+ }
+
+ function onDragEnd() {
+ return function() {
+ var brushed = __.data,
+ arc = arcs[arcs.active];
+
+ // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
+ // considered a drag without move. So we have to deal with that case
+ if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
+ removeStrum(arcs);
+ }
+
+ if (arc) {
+ var angle = arcs.startAngle(arcs.active);
+
+ arc.startAngle = angle;
+ arc.endAngle = angle;
+ arc.arc
+ .outerRadius(arcs.length(arcs.active))
+ .startAngle(angle)
+ .endAngle(angle);
+ }
+
+
+ brushed = selected(arcs);
+ arcs.active = undefined;
+ __.brushed = brushed;
+ pc.renderBrushed();
+ events.brushend.call(pc, __.brushed);
+ };
+ }
+
+ function brushReset(arcs) {
+ return function() {
+ var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ ids.forEach(function(d) {
+ arcs.active = d;
+ removeStrum(arcs);
+ });
+ onDragEnd(arcs)();
+ };
+ }
+
+ function install() {
+ var drag = d3.behavior.drag();
+
+ // Map of current arcs. arcs are stored per segment of the PC. A segment,
+ // being the area between two axes. The left most area is indexed at 0.
+ arcs.active = undefined;
+ // Returns the width of the PC segment where currently a arc is being
+ // placed. NOTE: even though they are evenly spaced in our current
+ // implementation, we keep for when non-even spaced segments are supported as
+ // well.
+ arcs.width = function(id) {
+ var arc = arcs[id];
+
+ if (arc === undefined) {
+ return undefined;
+ }
+
+ return arc.maxX - arc.minX;
+ };
+
+ // returns angles in [-PI/2, PI/2]
+ angle = function(p1, p2) {
+ var a = p1[0] - p2[0],
+ b = p1[1] - p2[1],
+ c = hypothenuse(a, b);
+
+ return Math.asin(b/c);
+ }
+
+ // returns angles in [0, 2 * PI]
+ arcs.endAngle = function(id) {
+ var arc = arcs[id];
+ if (arc === undefined) {
+ return undefined;
+ }
+ var sAngle = angle(arc.p1, arc.p2),
+ uAngle = -sAngle + Math.PI / 2;
+
+ if (arc.p1[0] > arc.p2[0]) {
+ uAngle = 2 * Math.PI - uAngle;
+ }
+
+ return uAngle;
+ }
+
+ arcs.startAngle = function(id) {
+ var arc = arcs[id];
+ if (arc === undefined) {
+ return undefined;
+ }
+
+ var sAngle = angle(arc.p1, arc.p3),
+ uAngle = -sAngle + Math.PI / 2;
+
+ if (arc.p1[0] > arc.p3[0]) {
+ uAngle = 2 * Math.PI - uAngle;
+ }
+
+ return uAngle;
+ }
+
+ arcs.length = function(id) {
+ var arc = arcs[id];
+
+ if (arc === undefined) {
+ return undefined;
+ }
+
+ var a = arc.p1[0] - arc.p2[0],
+ b = arc.p1[1] - arc.p2[1],
+ c = hypothenuse(a, b);
+
+ return(c);
+ }
+
+ pc.on("axesreorder.arcs", function() {
+ var ids = Object.getOwnPropertyNames(arcs).filter(function(d) {
+ return !isNaN(d);
+ });
+
+ // Checks if the first dimension is directly left of the second dimension.
+ function consecutive(first, second) {
+ var length = __.dimensions.length;
+ return __.dimensions.some(function(d, i) {
+ return (d === first)
+ ? i + i < length && __.dimensions[i + 1] === second
+ : false;
+ });
+ }
+
+ if (ids.length > 0) { // We have some arcs, which might need to be removed.
+ ids.forEach(function(d) {
+ var dims = arcs[d].dims;
+ arcs.active = d;
+ // If the two dimensions of the current arc are not next to each other
+ // any more, than we'll need to remove the arc. Otherwise we keep it.
+ if (!consecutive(dims.left, dims.right)) {
+ removeStrum(arcs);
+ }
+ });
+ onDragEnd(arcs)();
+ }
+ });
+
+ // Add a new svg group in which we draw the arcs.
+ pc.selection.select("svg").append("g")
+ .attr("id", "arcs")
+ .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ // Install the required brushReset function
+ pc.brushReset = brushReset(arcs);
+
+ drag
+ .on("dragstart", onDragStart(arcs))
+ .on("drag", onDrag(arcs))
+ .on("dragend", onDragEnd(arcs));
+
+ // NOTE: The styling needs to be done here and not in the css. This is because
+ // for 1D brushing, the canvas layers should not listen to
+ // pointer-events.
+ strumRect = pc.selection.select("svg").insert("rect", "g#arcs")
+ .attr("id", "arc-events")
+ .attr("x", __.margin.left)
+ .attr("y", __.margin.top)
+ .attr("width", w())
+ .attr("height", h() + 2)
+ .style("opacity", 0)
+ .call(drag);
+ }
+
+ brush.modes["angular"] = {
+ install: install,
+ uninstall: function() {
+ pc.selection.select("svg").select("g#arcs").remove();
+ pc.selection.select("svg").select("rect#arc-events").remove();
+ pc.on("axesreorder.arcs", undefined);
+ delete pc.brushReset;
+
+ strumRect = undefined;
+ },
+ selected: selected,
+ brushState: function () { return arcs; }
+ };
+
+ }());
+
+ pc.interactive = function() {
+ flags.interactive = true;
+ return this;
+ };
+
+ // expose a few objects
+ pc.xscale = xscale;
+ pc.yscale = yscale;
+ pc.ctx = ctx;
+ pc.canvas = canvas;
+ pc.g = function() { return g; };
+
+ // rescale for height, width and margins
+ // TODO currently assumes chart is brushable, and destroys old brushes
+ pc.resize = function() {
+ // selection size
+ pc.selection.select("svg")
+ .attr("width", __.width)
+ .attr("height", __.height)
+ pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")");
+
+ // FIXME: the current brush state should pass through
+ if (flags.brushable) pc.brushReset();
+
+ // scales
+ pc.autoscale();
+
+ // axes, destroys old brushes.
+ if (g) pc.createAxes();
+ if (flags.brushable) pc.brushable();
+ if (flags.reorderable) pc.reorderable();
+
+ events.resize.call(this, {width: __.width, height: __.height, margin: __.margin});
+ return this;
+ };
+
+ // highlight an array of data
+ pc.highlight = function(data) {
+ if (arguments.length === 0) {
+ return __.highlighted;
+ }
+
+ __.highlighted = data;
+ pc.clear("highlight");
+ d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", true);
+ data.forEach(path_highlight);
+ events.highlight.call(this, data);
+ return this;
+ };
+
+ // clear highlighting
+ pc.unhighlight = function() {
+ __.highlighted = [];
+ pc.clear("highlight");
+ d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", false);
+ return this;
+ };
+
+ // calculate 2d intersection of line a->b with line c->d
+ // points are objects with x and y properties
+ pc.intersection = function(a, b, c, d) {
+ return {
+ x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
+ y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x))
+ };
+ };
+
+ function position(d) {
+ var v = dragging[d];
+ return v == null ? xscale(d) : v;
+ }
+ pc.version = "0.7.0";
+ // this descriptive text should live with other introspective methods
+ pc.toString = function() { return "Parallel Coordinates: " + __.dimensions.length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; };
+
+ return pc;
+ };
+
+ d3.renderQueue = (function(func) {
+ var _queue = [], // data to be rendered
+ _rate = 10, // number of calls per frame
+ _clear = function() {}, // clearing function
+ _i = 0; // current iteration
+
+ var rq = function(data) {
+ if (data) rq.data(data);
+ rq.invalidate();
+ _clear();
+ rq.render();
+ };
+
+ rq.render = function() {
+ _i = 0;
+ var valid = true;
+ rq.invalidate = function() { valid = false; };
+
+ function doFrame() {
+ if (!valid) return true;
+ if (_i > _queue.length) return true;
+
+ // Typical d3 behavior is to pass a data item *and* its index. As the
+ // render queue splits the original data set, we'll have to be slightly
+ // more carefull about passing the correct index with the data item.
+ var end = Math.min(_i + _rate, _queue.length);
+ for (var i = _i; i < end; i++) {
+ func(_queue[i], i);
+ }
+ _i += _rate;
+ }
+
+ d3.timer(doFrame);
+ };
+
+ rq.data = function(data) {
+ rq.invalidate();
+ _queue = data.slice(0);
+ return rq;
+ };
+
+ rq.rate = function(value) {
+ if (!arguments.length) return _rate;
+ _rate = value;
+ return rq;
+ };
+
+ rq.remaining = function() {
+ return _queue.length - _i;
+ };
+
+ // clear the canvas
+ rq.clear = function(func) {
+ if (!arguments.length) {
+ _clear();
+ return rq;
+ }
+ _clear = func;
+ return rq;
+ };
+
+ rq.invalidate = function() {};
+
+ return rq;
+ });
diff --git a/dashed/assets/vendor/parallel_coordinates/divgrid.js b/dashed/assets/vendor/parallel_coordinates/divgrid.js
new file mode 100644
index 000000000..e4086e8ba
--- /dev/null
+++ b/dashed/assets/vendor/parallel_coordinates/divgrid.js
@@ -0,0 +1,59 @@
+// from http://bl.ocks.org/3687826
+module.exports = function(config) {
+ var columns = [];
+
+ var dg = function(selection) {
+ if (columns.length == 0) columns = d3.keys(selection.data()[0][0]);
+
+ // header
+ selection.selectAll(".header")
+ .data([true])
+ .enter().append("div")
+ .attr("class", "header")
+
+ var header = selection.select(".header")
+ .selectAll(".cell")
+ .data(columns);
+
+ header.enter().append("div")
+ .attr("class", function(d,i) { return "col-" + i; })
+ .classed("cell", true)
+
+ selection.selectAll(".header .cell")
+ .text(function(d) { return d; });
+
+ header.exit().remove();
+
+ // rows
+ var rows = selection.selectAll(".row")
+ .data(function(d) { return d; })
+
+ rows.enter().append("div")
+ .attr("class", "row")
+
+ rows.exit().remove();
+
+ var cells = selection.selectAll(".row").selectAll(".cell")
+ .data(function(d) { return columns.map(function(col){return d[col];}) })
+
+ // cells
+ cells.enter().append("div")
+ .attr("class", function(d,i) { return "col-" + i; })
+ .classed("cell", true)
+
+ cells.exit().remove();
+
+ selection.selectAll(".cell")
+ .text(function(d) { return d; });
+
+ return dg;
+ };
+
+ dg.columns = function(_) {
+ if (!arguments.length) return columns;
+ columns = _;
+ return this;
+ };
+
+ return dg;
+};
diff --git a/dashed/assets/vendor/pygments.css b/dashed/assets/vendor/pygments.css
new file mode 100644
index 000000000..ef95359ff
--- /dev/null
+++ b/dashed/assets/vendor/pygments.css
@@ -0,0 +1,62 @@
+.codehilite .hll { background-color: #ffffcc }
+.codehilite { background: #f8f8f8; }
+.codehilite .c { color: #408080; font-style: italic } /* Comment */
+.codehilite .err { border: 1px solid #FF0000 } /* Error */
+.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
+.codehilite .o { color: #666666 } /* Operator */
+.codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
+.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
+.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
+.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
+.codehilite .gd { color: #A00000 } /* Generic.Deleted */
+.codehilite .ge { font-style: italic } /* Generic.Emph */
+.codehilite .gr { color: #FF0000 } /* Generic.Error */
+.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
+.codehilite .gi { color: #00A000 } /* Generic.Inserted */
+.codehilite .go { color: #808080 } /* Generic.Output */
+.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
+.codehilite .gs { font-weight: bold } /* Generic.Strong */
+.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
+.codehilite .gt { color: #0040D0 } /* Generic.Traceback */
+.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
+.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
+.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
+.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
+.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
+.codehilite .kt { color: #B00040 } /* Keyword.Type */
+.codehilite .m { color: #666666 } /* Literal.Number */
+.codehilite .s { color: #BA2121 } /* Literal.String */
+.codehilite .na { color: #7D9029 } /* Name.Attribute */
+.codehilite .nb { color: #008000 } /* Name.Builtin */
+.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
+.codehilite .no { color: #880000 } /* Name.Constant */
+.codehilite .nd { color: #AA22FF } /* Name.Decorator */
+.codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
+.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
+.codehilite .nf { color: #0000FF } /* Name.Function */
+.codehilite .nl { color: #A0A000 } /* Name.Label */
+.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
+.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
+.codehilite .nv { color: #19177C } /* Name.Variable */
+.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
+.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
+.codehilite .mf { color: #666666 } /* Literal.Number.Float */
+.codehilite .mh { color: #666666 } /* Literal.Number.Hex */
+.codehilite .mi { color: #666666 } /* Literal.Number.Integer */
+.codehilite .mo { color: #666666 } /* Literal.Number.Oct */
+.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
+.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
+.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
+.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
+.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
+.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
+.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
+.codehilite .sx { color: #008000 } /* Literal.String.Other */
+.codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
+.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
+.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
+.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
+.codehilite .vc { color: #19177C } /* Name.Variable.Class */
+.codehilite .vg { color: #19177C } /* Name.Variable.Global */
+.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
+.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
diff --git a/dashed/assets/vendor/select2.sortable.js b/dashed/assets/vendor/select2.sortable.js
new file mode 100644
index 000000000..18fac5104
--- /dev/null
+++ b/dashed/assets/vendor/select2.sortable.js
@@ -0,0 +1,146 @@
+/**
+ * jQuery Select2 Sortable
+ * - enable select2 to be sortable via normal select element
+ *
+ * author : Vafour
+ * modified : Kevin Provance (kprovance)
+ * inspired by : jQuery Chosen Sortable (https://github.com/mrhenry/jquery-chosen-sortable)
+ * License : GPL
+ */
+
+(function ($) {
+ $.fn.extend({
+ select2SortableOrder: function () {
+ var $this = this.filter('[multiple]');
+
+ $this.each(function () {
+ var $select = $(this);
+
+ // skip elements not select2-ed
+ if (typeof ($select.data('select2')) !== 'object') {
+ return false;
+ }
+
+ var $select2 = $select.siblings('.select2-container');
+ var sorted;
+
+ // Opt group names
+ var optArr = [];
+
+ $select.find('optgroup').each(function(idx, val) {
+ optArr.push (val);
+ });
+
+ $select.find('option').each(function(idx, val) {
+ var groupName = $(this).parent('optgroup').prop('label');
+ var optVal = this;
+
+ if (groupName === undefined) {
+ if (this.value !== '' && !this.selected) {
+ optArr.push (optVal);
+ }
+ }
+ });
+
+ sorted = $($select2.find('.select2-choices li[class!="select2-search-field"]').map(function () {
+ if (!this) {
+ return undefined;
+ }
+
+ var id = $(this).data('select2Data').id;
+
+ return $select.find('option[value="' + id + '"]')[0];
+ }));
+
+ sorted.push.apply(sorted, optArr);
+
+ $select.children().remove();
+ $select.append(sorted);
+ });
+
+ return $this;
+ },
+
+ select2Sortable: function () {
+ var args = Array.prototype.slice.call(arguments, 0);
+ var $this = this.filter('[multiple]'),
+ validMethods = ['destroy'];
+
+ if (args.length === 0 || typeof (args[0]) === 'object') {
+ var defaultOptions = {
+ bindOrder: 'formSubmit', // or sortableStop
+ sortableOptions: {
+ placeholder: 'ui-state-highlight',
+ items: 'li:not(.select2-search-field)',
+ tolerance: 'pointer'
+ }
+ };
+
+ var options = $.extend(defaultOptions, args[0]);
+
+ // Init select2 only if not already initialized to prevent select2 configuration loss
+ if (typeof ($this.data('select2')) !== 'object') {
+ $this.select2();
+ }
+
+ $this.each(function () {
+ var $select = $(this)
+ var $select2choices = $select.siblings('.select2-container').find('.select2-choices');
+
+ // Init jQuery UI Sortable
+ $select2choices.sortable(options.sortableOptions);
+
+ switch (options.bindOrder) {
+ case 'sortableStop':
+ // apply options ordering in sortstop event
+ $select2choices.on("sortstop.select2sortable", function (event, ui) {
+ $select.select2SortableOrder();
+ });
+
+ $select.on('change', function (e) {
+ $(this).select2SortableOrder();
+ });
+ break;
+
+ default:
+ // apply options ordering in form submit
+ $select.closest('form').unbind('submit.select2sortable').on('submit.select2sortable', function () {
+ $select.select2SortableOrder();
+ });
+ break;
+ }
+ });
+ }
+ else if (typeof (args[0] === 'string')) {
+ if ($.inArray(args[0], validMethods) == -1) {
+ throw "Unknown method: " + args[0];
+ }
+
+ if (args[0] === 'destroy') {
+ $this.select2SortableDestroy();
+ }
+ }
+
+ return $this;
+ },
+
+ select2SortableDestroy: function () {
+ var $this = this.filter('[multiple]');
+ $this.each(function () {
+ var $select = $(this)
+ var $select2choices = $select.parent().find('.select2-choices');
+
+ // unbind form submit event
+ $select.closest('form').unbind('submit.select2sortable');
+
+ // unbind sortstop event
+ $select2choices.unbind("sortstop.select2sortable");
+
+ // destroy select2Sortable
+ $select2choices.sortable('destroy');
+ });
+
+ return $this;
+ }
+ });
+}(jQuery));
diff --git a/dashed/assets/visualizations/big_number.css b/dashed/assets/visualizations/big_number.css
new file mode 100644
index 000000000..872a5715a
--- /dev/null
+++ b/dashed/assets/visualizations/big_number.css
@@ -0,0 +1,26 @@
+.big_number g.axis text {
+ font-size: 10px;
+ font-weight: normal;
+ color: gray;
+ fill: gray;
+ text-anchor: middle;
+ alignment-baseline: middle;
+ font-weight: none;
+}
+
+.big_number text.big {
+ stroke: black;
+ text-anchor: middle;
+ fill: black;
+}
+
+.big_number g.tick line {
+ stroke-width: 1px;
+ stroke: grey;
+}
+
+.big_number .domain {
+ fill: none;
+ stroke: black;
+ stroke-width: 1;
+}
diff --git a/dashed/assets/visualizations/big_number.js b/dashed/assets/visualizations/big_number.js
new file mode 100644
index 000000000..7f6bfcd4b
--- /dev/null
+++ b/dashed/assets/visualizations/big_number.js
@@ -0,0 +1,162 @@
+// JS
+var d3 = window.d3 || require('d3');
+
+// CSS
+require('./big_number.css');
+
+var px = require('../javascripts/modules/dashed.js');
+
+function bigNumberVis(slice) {
+ var div = d3.select(slice.selector);
+
+ function render() {
+ d3.json(slice.jsonEndpoint(), function (error, payload) {
+ //Define the percentage bounds that define color from red to green
+ if (error !== null) {
+ slice.error(error.responseText);
+ return '';
+ }
+ var fd = payload.form_data;
+ var json = payload.data;
+ var color_range = [-1, 1];
+
+ var f = d3.format(fd.y_axis_format);
+ var fp = d3.format('+.1%');
+ var width = slice.width();
+ var height = slice.height();
+ var svg = div.append('svg');
+ svg.attr("width", width);
+ svg.attr("height", height);
+ var data = json.data;
+ var compare_suffix = ' ' + json.compare_suffix;
+ var v_compare = null;
+ var v = data[data.length - 1][1];
+ if (json.compare_lag > 0) {
+ var pos = data.length - (json.compare_lag + 1);
+ if (pos >= 0) {
+ v_compare = (v / data[pos][1]) - 1;
+ }
+ }
+ var date_ext = d3.extent(data, function (d) {
+ return d[0];
+ });
+ var value_ext = d3.extent(data, function (d) {
+ return d[1];
+ });
+
+ var margin = 20;
+ var scale_x = d3.time.scale.utc().domain(date_ext).range([margin, width - margin]);
+ var scale_y = d3.scale.linear().domain(value_ext).range([height - (margin), margin]);
+ var colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
+ var scale_color = d3.scale
+ .linear().domain(color_range)
+ .interpolate(d3.interpolateHsl)
+ .range(colorRange).clamp(true);
+ var line = d3.svg.line()
+ .x(function (d) {
+ return scale_x(d[0]);
+ })
+ .y(function (d) {
+ return scale_y(d[1]);
+ })
+ .interpolate("basis");
+
+ //Drawing trend line
+ var g = svg.append('g');
+
+ g.append('path')
+ .attr('d', function (d) {
+ return line(data);
+ })
+ .attr('stroke-width', 5)
+ .attr('opacity', 0.5)
+ .attr('fill', "none")
+ .attr('stroke-linecap', "round")
+ .attr('stroke', "grey");
+
+ g = svg.append('g')
+ .attr('class', 'digits')
+ .attr('opacity', 1);
+
+ var y = height / 2;
+ if (v_compare !== null) {
+ y = (height / 8) * 3;
+ }
+
+ //Printing big number
+ g.append('text')
+ .attr('x', width / 2)
+ .attr('y', y)
+ .attr('class', 'big')
+ .attr('alignment-baseline', 'middle')
+ .attr('id', 'bigNumber')
+ .style('font-weight', 'bold')
+ .style('cursor', 'pointer')
+ .text(f(v))
+ .style('font-size', d3.min([height, width]) / 3.5)
+ .attr('fill', 'white');
+
+ var c = scale_color(v_compare);
+
+ //Printing compare %
+ if (v_compare !== null) {
+ g.append('text')
+ .attr('x', width / 2)
+ .attr('y', (height / 16) * 12)
+ .text(fp(v_compare) + compare_suffix)
+ .style('font-size', d3.min([height, width]) / 8)
+ .style('text-anchor', 'middle')
+ .attr('fill', c)
+ .attr('stroke', c);
+ }
+
+ var g_axis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
+ g = g_axis.append('g');
+ var x_axis = d3.svg.axis()
+ .scale(scale_x)
+ .orient('bottom')
+ .ticks(4)
+ .tickFormat(px.formatDate);
+ g.call(x_axis);
+ g.attr('transform', 'translate(0,' + (height - margin) + ')');
+
+ g = g_axis.append('g').attr('transform', 'translate(' + (width - margin) + ',0)');
+ var y_axis = d3.svg.axis()
+ .scale(scale_y)
+ .orient('left')
+ .tickFormat(d3.format(fd.y_axis_format))
+ .tickValues(value_ext);
+ g.call(y_axis);
+ g.selectAll('text')
+ .style('text-anchor', 'end')
+ .attr('y', '-7')
+ .attr('x', '-4');
+
+ g.selectAll("text")
+ .style('font-size', '10px');
+
+ div.on('mouseover', function (d) {
+ var div = d3.select(this);
+ div.select('path').transition().duration(500).attr('opacity', 1)
+ .style('stroke-width', '2px');
+ div.select('g.digits').transition().duration(500).attr('opacity', 0.1);
+ div.select('g.axis').transition().duration(500).attr('opacity', 1);
+ })
+ .on('mouseout', function (d) {
+ var div = d3.select(this);
+ div.select('path').transition().duration(500).attr('opacity', 0.5)
+ .style('stroke-width', '5px');
+ div.select('g.digits').transition().duration(500).attr('opacity', 1);
+ div.select('g.axis').transition().duration(500).attr('opacity', 0);
+ });
+ slice.done(payload);
+ });
+ }
+
+ return {
+ render: render,
+ resize: render
+ };
+}
+
+module.exports = bigNumberVis;
diff --git a/dashed/assets/visualizations/directed_force.css b/dashed/assets/visualizations/directed_force.css
new file mode 100644
index 000000000..170eccba1
--- /dev/null
+++ b/dashed/assets/visualizations/directed_force.css
@@ -0,0 +1,19 @@
+.directed_force path.link {
+ fill: none;
+ stroke: #000;
+ stroke-width: 1.5px;
+}
+
+.directed_force circle {
+ fill: #ccc;
+ stroke: #000;
+ stroke-width: 1.5px;
+ stroke-opacity: 1;
+ opacity: 0.75;
+}
+
+.directed_force text {
+ fill: #000;
+ font: 10px sans-serif;
+ pointer-events: none;
+}
diff --git a/dashed/assets/visualizations/directed_force.js b/dashed/assets/visualizations/directed_force.js
new file mode 100644
index 000000000..a0252067e
--- /dev/null
+++ b/dashed/assets/visualizations/directed_force.js
@@ -0,0 +1,175 @@
+// JS
+var d3 = window.d3 || require('d3');
+
+// CSS
+require('./directed_force.css');
+
+/* Modified from http://bl.ocks.org/d3noob/5141278 */
+function directedForceVis(slice) {
+ var div = d3.select(slice.selector);
+ var link_length = slice.data.form_data.link_length || 200;
+ var charge = slice.data.form_data.charge || -500;
+
+ var render = function () {
+ var width = slice.width();
+ var height = slice.height() - 25;
+ d3.json(slice.jsonEndpoint(), function (error, json) {
+
+ if (error !== null) {
+ slice.error(error.responseText);
+ return '';
+ }
+ var links = json.data;
+ var nodes = {};
+ // Compute the distinct nodes from the links.
+ links.forEach(function (link) {
+ link.source = nodes[link.source] || (nodes[link.source] = {
+ name: link.source
+ });
+ link.target = nodes[link.target] || (nodes[link.target] = {
+ name: link.target
+ });
+ link.value = Number(link.value);
+
+ var target_name = link.target.name;
+ var source_name = link.source.name;
+
+ if (nodes[target_name].total === undefined) {
+ nodes[target_name].total = link.value;
+ }
+ if (nodes[source_name].total === undefined) {
+ nodes[source_name].total = 0;
+ }
+ if (nodes[target_name].max === undefined) {
+ nodes[target_name].max = 0;
+ }
+ if (link.value > nodes[target_name].max) {
+ nodes[target_name].max = link.value;
+ }
+ if (nodes[target_name].min === undefined) {
+ nodes[target_name].min = 0;
+ }
+ if (link.value > nodes[target_name].min) {
+ nodes[target_name].min = link.value;
+ }
+
+ nodes[target_name].total += link.value;
+ });
+
+ var force = d3.layout.force()
+ .nodes(d3.values(nodes))
+ .links(links)
+ .size([width, height])
+ .linkDistance(link_length)
+ .charge(charge)
+ .on("tick", tick)
+ .start();
+
+ var svg = div.append("svg")
+ .attr("width", width)
+ .attr("height", height);
+
+ // build the arrow.
+ svg.append("svg:defs").selectAll("marker")
+ .data(["end"]) // Different link/path types can be defined here
+ .enter().append("svg:marker") // This section adds in the arrows
+ .attr("id", String)
+ .attr("viewBox", "0 -5 10 10")
+ .attr("refX", 15)
+ .attr("refY", -1.5)
+ .attr("markerWidth", 6)
+ .attr("markerHeight", 6)
+ .attr("orient", "auto")
+ .append("svg:path")
+ .attr("d", "M0,-5L10,0L0,5");
+
+ var edgeScale = d3.scale.linear()
+ .range([0.1, 0.5]);
+ // add the links and the arrows
+ var path = svg.append("svg:g").selectAll("path")
+ .data(force.links())
+ .enter().append("svg:path")
+ .attr("class", "link")
+ .style("opacity", function (d) {
+ return edgeScale(d.value / d.target.max);
+ })
+ .attr("marker-end", "url(#end)");
+
+ // define the nodes
+ var node = svg.selectAll(".node")
+ .data(force.nodes())
+ .enter().append("g")
+ .attr("class", "node")
+ .on("mouseenter", function (d) {
+ d3.select(this)
+ .select("circle")
+ .transition()
+ .style('stroke-width', 5);
+
+ d3.select(this)
+ .select("text")
+ .transition()
+ .style('font-size', 25);
+ })
+ .on("mouseleave", function (d) {
+ d3.select(this)
+ .select("circle")
+ .transition()
+ .style('stroke-width', 1.5);
+ d3.select(this)
+ .select("text")
+ .transition()
+ .style('font-size', 12);
+ })
+ .call(force.drag);
+
+ // add the nodes
+ var ext = d3.extent(d3.values(nodes), function (d) {
+ return Math.sqrt(d.total);
+ });
+ var circleScale = d3.scale.linear()
+ .domain(ext)
+ .range([3, 30]);
+
+ node.append("circle")
+ .attr("r", function (d) {
+ return circleScale(Math.sqrt(d.total));
+ });
+
+ // add the text
+ node.append("text")
+ .attr("x", 6)
+ .attr("dy", ".35em")
+ .text(function (d) {
+ return d.name;
+ });
+
+ // add the curvy lines
+ function tick() {
+ path.attr("d", function (d) {
+ var dx = d.target.x - d.source.x,
+ dy = d.target.y - d.source.y,
+ dr = Math.sqrt(dx * dx + dy * dy);
+ return "M" +
+ d.source.x + "," +
+ d.source.y + "A" +
+ dr + "," + dr + " 0 0,1 " +
+ d.target.x + "," +
+ d.target.y;
+ });
+
+ node.attr("transform", function (d) {
+ return "translate(" + d.x + "," + d.y + ")";
+ });
+ }
+
+ slice.done(json);
+ });
+ };
+ return {
+ render: render,
+ resize: render
+ };
+}
+
+module.exports = directedForceVis;
diff --git a/dashed/assets/visualizations/filter_box.css b/dashed/assets/visualizations/filter_box.css
new file mode 100644
index 000000000..156978361
--- /dev/null
+++ b/dashed/assets/visualizations/filter_box.css
@@ -0,0 +1,8 @@
+.select2-highlighted > .filter_box {
+ background-color: transparent;
+ border: 1px dashed black;
+}
+
+.dashboard .filter_box .slice_container > div {
+ padding-top: 0;
+}
diff --git a/dashed/assets/visualizations/filter_box.js b/dashed/assets/visualizations/filter_box.js
new file mode 100644
index 000000000..64959a4b2
--- /dev/null
+++ b/dashed/assets/visualizations/filter_box.js
@@ -0,0 +1,82 @@
+// JS
+var $ = window.$ = require('jquery');
+var jQuery = window.jQuery = $;
+var d3 = window.d3 || require('d3');
+
+// CSS
+require('./filter_box.css');
+require('../javascripts/dashed-select2.js');
+
+function filterBox(slice) {
+ var filtersObj = {};
+ var d3token = d3.select(slice.selector);
+
+ var fltChanged = function () {
+ var val = $(this).val();
+ var vals = [];
+ if (val !== '') {
+ vals = val.split(',');
+ }
+ slice.setFilter($(this).attr('name'), vals);
+ };
+
+ var refresh = function () {
+ d3token.selectAll("*").remove();
+ var container = d3token
+ .append('div')
+ .classed('padded', true);
+
+ $.getJSON(slice.jsonEndpoint(), function (payload) {
+ var maxes = {};
+
+ for (var filter in payload.data) {
+ var data = payload.data[filter];
+ maxes[filter] = d3.max(data, function (d) {
+ return d.metric;
+ });
+ var id = 'fltbox__' + filter;
+
+ var div = container.append('div');
+
+ div.append("label").text(filter);
+
+ div.append('div')
+ .attr('name', filter)
+ .classed('form-control', true)
+ .attr('multiple', '')
+ .attr('id', id);
+
+ filtersObj[filter] = $('#' + id).select2({
+ placeholder: "Select [" + filter + ']',
+ containment: 'parent',
+ dropdownAutoWidth: true,
+ data: data,
+ multiple: true,
+ formatResult: select2Formatter
+ })
+ .on('change', fltChanged);
+ }
+ slice.done();
+
+ function select2Formatter(result, container /*, query, escapeMarkup*/) {
+ var perc = Math.round((result.metric / maxes[result.filter]) * 100);
+ var style = 'padding: 2px 5px;';
+ style += "background-image: ";
+ style += "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
+
+ $(container).attr('style', 'padding: 0px; background: white;');
+ $(container).addClass('filter_box');
+ return '' + result.text + '
';
+ }
+ })
+ .fail(function (xhr) {
+ slice.error(xhr.responseText);
+ });
+ };
+ return {
+ render: refresh,
+ resize: refresh
+ };
+}
+
+module.exports = filterBox;
diff --git a/dashed/assets/visualizations/heatmap.css b/dashed/assets/visualizations/heatmap.css
new file mode 100644
index 000000000..bce124821
--- /dev/null
+++ b/dashed/assets/visualizations/heatmap.css
@@ -0,0 +1,79 @@
+.heatmap .axis text {
+ font: 10px sans-serif;
+}
+
+.heatmap .axis path,
+.heatmap .axis line {
+ fill: none;
+ stroke: #000;
+ shape-rendering: crispEdges;
+}
+
+.heatmap svg {
+}
+
+.heatmap canvas, .heatmap img {
+ image-rendering: optimizeSpeed; /* Older versions of FF */
+ image-rendering: -moz-crisp-edges; /* FF 6.0+ */
+ image-rendering: -webkit-optimize-contrast; /* Safari */
+ image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */
+ image-rendering: pixelated; /* Awesome future-browsers */
+ -ms-interpolation-mode: nearest-neighbor; /* IE */
+}
+
+/* from d3-tip */
+.d3-tip {
+ line-height: 1;
+ font-weight: bold;
+ padding: 12px;
+ background: rgba(0, 0, 0, 0.8);
+ color: #fff;
+ border-radius: 2px;
+ pointer-events: none;
+}
+
+/* Creates a small triangle extender for the tooltip */
+.d3-tip:after {
+ box-sizing: border-box;
+ display: inline;
+ font-size: 10px;
+ width: 100%;
+ line-height: 1;
+ color: rgba(0, 0, 0, 0.8);
+ position: absolute;
+ pointer-events: none;
+}
+
+/* Northward tooltips */
+.d3-tip.n:after {
+ content: "\25BC";
+ margin: -1px 0 0 0;
+ top: 100%;
+ left: 0;
+ text-align: center;
+}
+
+/* Eastward tooltips */
+.d3-tip.e:after {
+ content: "\25C0";
+ margin: -4px 0 0 0;
+ top: 50%;
+ left: -8px;
+}
+
+/* Southward tooltips */
+.d3-tip.s:after {
+ content: "\25B2";
+ margin: 0 0 1px 0;
+ top: -8px;
+ left: 0;
+ text-align: center;
+}
+
+/* Westward tooltips */
+.d3-tip.w:after {
+ content: "\25B6";
+ margin: -4px 0 0 -1px;
+ top: 50%;
+ left: 100%;
+}
diff --git a/dashed/assets/visualizations/heatmap.js b/dashed/assets/visualizations/heatmap.js
new file mode 100644
index 000000000..6bc688fc8
--- /dev/null
+++ b/dashed/assets/visualizations/heatmap.js
@@ -0,0 +1,209 @@
+// JS
+var $ = window.$ || require('jquery');
+var px = window.px || require('../javascripts/modules/dashed.js');
+var d3 = require('d3');
+
+d3.tip = require('d3-tip'); //using window.d3 doesn't capture events properly bc of multiple instances
+
+// CSS
+require('./heatmap.css');
+
+// Inspired from http://bl.ocks.org/mbostock/3074470
+// https://jsfiddle.net/cyril123/h0reyumq/
+function heatmapVis(slice) {
+ var margins = {
+ t: 10,
+ r: 10,
+ b: 50,
+ l: 60
+ };
+
+ function refresh() {
+ var width = slice.width();
+ var height = slice.height();
+ var hmWidth = width - (margins.l + margins.r);
+ var hmHeight = height - (margins.b + margins.t);
+ var fp = d3.format('.3p');
+ d3.json(slice.jsonEndpoint(), function (error, payload) {
+ var matrix = {};
+ if (error) {
+ slice.error(error.responseText);
+ return '';
+ }
+ var fd = payload.form_data;
+ var data = payload.data;
+
+ function ordScale(k, rangeBands, reverse) {
+ if (reverse === undefined) {
+ reverse = false;
+ }
+ var domain = {};
+ $.each(data, function (i, d) {
+ domain[d[k]] = true;
+ });
+ domain = Object.keys(domain).sort(function (a, b) {
+ return b - a;
+ });
+ if (reverse) {
+ domain.reverse();
+ }
+ if (rangeBands === undefined) {
+ return d3.scale.ordinal().domain(domain).range(d3.range(domain.length));
+ } else {
+ return d3.scale.ordinal().domain(domain).rangeBands(rangeBands);
+ }
+ }
+ var xScale = ordScale('x');
+ var yScale = ordScale('y', undefined, true);
+ var xRbScale = ordScale('x', [0, hmWidth]);
+ var yRbScale = ordScale('y', [hmHeight, 0]);
+ var X = 0,
+ Y = 1;
+ var heatmapDim = [xRbScale.domain().length, yRbScale.domain().length];
+
+ var color = px.color.colorScalerFactory(fd.linear_color_scheme);
+
+ var scale = [
+ d3.scale.linear()
+ .domain([0, heatmapDim[X]])
+ .range([0, hmWidth]),
+ d3.scale.linear()
+ .domain([0, heatmapDim[Y]])
+ .range([0, hmHeight])
+ ];
+
+ var container = d3.select(slice.selector)
+ .style("left", "0px")
+ .style("position", "relative")
+ .style("top", "0px");
+
+ var canvas = container.append("canvas")
+ .attr("width", heatmapDim[X])
+ .attr("height", heatmapDim[Y])
+ .style("width", hmWidth + "px")
+ .style("height", hmHeight + "px")
+ .style("image-rendering", fd.canvas_image_rendering)
+ .style("left", margins.l + "px")
+ .style("top", margins.t + "px")
+ .style("position", "absolute");
+
+ var svg = container.append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .style("left", "0px")
+ .style("top", "0px")
+ .style("position", "absolute");
+
+ var rect = svg.append('g')
+ .attr("transform", "translate(" + margins.l + "," + margins.t + ")")
+ .append('rect')
+ .style('fill-opacity', 0)
+ .attr('stroke', 'black')
+ .attr("width", hmWidth)
+ .attr("height", hmHeight);
+
+ var tip = d3.tip()
+ .attr('class', 'd3-tip')
+ .offset(function () {
+ var k = d3.mouse(this);
+ var x = k[0] - (hmWidth / 2);
+ return [k[1] - 20, x];
+ })
+ .html(function (d) {
+ var k = d3.mouse(this);
+ var m = Math.floor(scale[0].invert(k[0]));
+ var n = Math.floor(scale[1].invert(k[1]));
+ if (m in matrix && n in matrix[m]) {
+ var obj = matrix[m][n];
+ var s = "";
+ s += "" + fd.all_columns_x + ": " + obj.x + "
";
+ s += "
" + fd.all_columns_y + ": " + obj.y + "
";
+ s += "
" + fd.metric + ": " + obj.v + "
";
+ s += "
%: " + fp(obj.perc) + "
";
+ return s;
+ }
+ });
+
+ rect.call(tip);
+
+ var xAxis = d3.svg.axis()
+ .scale(xRbScale)
+ .tickValues(xRbScale.domain().filter(
+ function (d, i) {
+ return !(i % (parseInt(fd.xscale_interval, 10)));
+ }))
+ .orient("bottom");
+ var yAxis = d3.svg.axis()
+ .scale(yRbScale)
+ .tickValues(yRbScale.domain().filter(
+ function (d, i) {
+ return !(i % (parseInt(fd.yscale_interval, 10)));
+ }))
+ .orient("left");
+
+ svg.append("g")
+ .attr("class", "x axis")
+ .attr("transform", "translate(" + margins.l + "," + (margins.t + hmHeight) + ")")
+ .call(xAxis)
+ .selectAll("text")
+ .style("text-anchor", "end")
+ .attr("transform", "rotate(-45)")
+ .style("font-weight", "bold");
+
+ svg.append("g")
+ .attr("class", "y axis")
+ .attr("transform", "translate(" + margins.l + ", 0)")
+ .call(yAxis);
+
+ rect.on('mousemove', tip.show);
+ rect.on('mouseout', tip.hide);
+
+ var context = canvas.node().getContext("2d");
+ context.imageSmoothingEnabled = false;
+ createImageObj();
+
+ // Compute the pixel colors; scaled by CSS.
+ function createImageObj() {
+ var imageObj = new Image();
+ var image = context.createImageData(heatmapDim[0], heatmapDim[1]);
+ var pixs = {};
+ $.each(data, function (i, d) {
+ var c = d3.rgb(color(d.perc));
+ var x = xScale(d.x);
+ var y = yScale(d.y);
+ pixs[x + (y * xScale.domain().length)] = c;
+ if (matrix[x] === undefined) {
+ matrix[x] = {};
+ }
+ if (matrix[x][y] === undefined) {
+ matrix[x][y] = d;
+ }
+ });
+
+ var p = -1;
+ for (var i = 0; i < heatmapDim[0] * heatmapDim[1]; i++) {
+ var c = pixs[i];
+ var alpha = 255;
+ if (c === undefined) {
+ c = d3.rgb('#F00');
+ alpha = 0;
+ }
+ image.data[++p] = c.r;
+ image.data[++p] = c.g;
+ image.data[++p] = c.b;
+ image.data[++p] = alpha;
+ }
+ context.putImageData(image, 0, 0);
+ imageObj.src = canvas.node().toDataURL();
+ }
+ slice.done();
+
+ });
+ }
+ return {
+ render: refresh,
+ resize: refresh
+ };
+}
+
+module.exports = heatmapVis;
diff --git a/dashed/assets/visualizations/iframe.js b/dashed/assets/visualizations/iframe.js
new file mode 100644
index 000000000..0f9fddc8b
--- /dev/null
+++ b/dashed/assets/visualizations/iframe.js
@@ -0,0 +1,25 @@
+var $ = window.$ || require('jquery');
+
+function iframeWidget(slice) {
+
+ function refresh() {
+ $('#code').attr('rows', '15');
+ $.getJSON(slice.jsonEndpoint(), function (payload) {
+ slice.container.html('
');
+ var iframe = slice.container.find('iframe');
+ iframe.css('height', slice.height());
+ iframe.attr('src', payload.form_data.url);
+ slice.done();
+ })
+ .fail(function (xhr) {
+ slice.error(xhr.responseText);
+ });
+ }
+
+ return {
+ render: refresh,
+ resize: refresh
+ };
+}
+
+module.exports = iframeWidget;
diff --git a/dashed/assets/visualizations/markup.js b/dashed/assets/visualizations/markup.js
new file mode 100644
index 000000000..f202c92dd
--- /dev/null
+++ b/dashed/assets/visualizations/markup.js
@@ -0,0 +1,23 @@
+var $ = window.$ || require('jquery');
+
+function markupWidget(slice) {
+
+ function refresh() {
+ $('#code').attr('rows', '15');
+
+ $.getJSON(slice.jsonEndpoint(), function (payload) {
+ slice.container.html(payload.data.html);
+ slice.done();
+ })
+ .fail(function (xhr) {
+ slice.error(xhr.responseText);
+ });
+ }
+
+ return {
+ render: refresh,
+ resize: refresh
+ };
+}
+
+module.exports = markupWidget;
diff --git a/dashed/assets/visualizations/nvd3_vis.css b/dashed/assets/visualizations/nvd3_vis.css
new file mode 100644
index 000000000..80c452b4d
--- /dev/null
+++ b/dashed/assets/visualizations/nvd3_vis.css
@@ -0,0 +1,8 @@
+g.dashed path {
+ stroke-dasharray: 5, 5;
+}
+
+.nvtooltip tr.highlight td {
+ font-weight: bold;
+ font-size: 15px !important;
+}
diff --git a/dashed/assets/visualizations/nvd3_vis.js b/dashed/assets/visualizations/nvd3_vis.js
new file mode 100644
index 000000000..04a0852ab
--- /dev/null
+++ b/dashed/assets/visualizations/nvd3_vis.js
@@ -0,0 +1,208 @@
+// JS
+var $ = window.$ || require('jquery');
+var d3 = window.d3 || require('d3');
+var px = window.px || require('../javascripts/modules/dashed.js');
+var nv = require('nvd3');
+
+// CSS
+require('../node_modules/nvd3/build/nv.d3.min.css');
+require('./nvd3_vis.css');
+
+function nvd3Vis(slice) {
+ var chart;
+
+ var render = function () {
+ $.getJSON(slice.jsonEndpoint(), function (payload) {
+ var fd = payload.form_data;
+ var viz_type = fd.viz_type;
+
+ var f = d3.format('.3s');
+ var colorKey = 'key';
+
+ nv.addGraph(function () {
+ switch (viz_type) {
+ case 'line':
+ if (fd.show_brush) {
+ chart = nv.models.lineWithFocusChart();
+ chart.lines2.xScale(d3.time.scale.utc());
+ chart.x2Axis
+ .showMaxMin(fd.x_axis_showminmax)
+ .staggerLabels(true);
+ } else {
+ chart = nv.models.lineChart();
+ }
+ // To alter the tooltip header
+ // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';});
+ chart.xScale(d3.time.scale.utc());
+ chart.interpolate(fd.line_interpolation);
+ chart.xAxis
+ .showMaxMin(fd.x_axis_showminmax)
+ .staggerLabels(true);
+ break;
+
+ case 'bar':
+ chart = nv.models.multiBarChart()
+ .showControls(true)
+ .groupSpacing(0.1);
+
+ chart.xAxis
+ .showMaxMin(false)
+ .staggerLabels(true);
+
+ chart.stacked(fd.bar_stacked);
+ break;
+
+ case 'dist_bar':
+ chart = nv.models.multiBarChart()
+ .showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode.
+ .reduceXTicks(false)
+ .rotateLabels(45)
+ .groupSpacing(0.1); //Distance between each group of bars.
+
+ chart.xAxis
+ .showMaxMin(false);
+
+ chart.stacked(fd.bar_stacked);
+ break;
+
+ case 'pie':
+ chart = nv.models.pieChart();
+ colorKey = 'x';
+ chart.valueFormat(f);
+ if (fd.donut) {
+ chart.donut(true);
+ chart.labelsOutside(true);
+ }
+ chart.labelsOutside(true);
+ chart.cornerRadius(true);
+ break;
+
+ case 'column':
+ chart = nv.models.multiBarChart()
+ .reduceXTicks(false)
+ .rotateLabels(45);
+ break;
+
+ case 'compare':
+ chart = nv.models.cumulativeLineChart();
+ chart.xScale(d3.time.scale.utc());
+ chart.xAxis
+ .showMaxMin(false)
+ .staggerLabels(true);
+ break;
+
+ case 'bubble':
+ var row = function (col1, col2) {
+ return "
" + col1 + " " + col2 + " ";
+ };
+ chart = nv.models.scatterChart();
+ chart.showDistX(true);
+ chart.showDistY(true);
+ chart.tooltip.contentGenerator(function (obj) {
+ var p = obj.point;
+ var s = "
";
+ s += '' + p[fd.entity] + ' (' + p.group + ') ';
+ s += row(fd.x, f(p.x));
+ s += row(fd.y, f(p.y));
+ s += row(fd.size, f(p.size));
+ s += "
";
+ return s;
+ });
+ chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]);
+ break;
+
+ case 'area':
+ chart = nv.models.stackedAreaChart();
+ chart.style(fd.stacked_style);
+ chart.xScale(d3.time.scale.utc());
+ chart.xAxis
+ .showMaxMin(false)
+ .staggerLabels(true);
+ break;
+
+ default:
+ throw new Error("Unrecognized visualization for nvd3" + viz_type);
+ }
+
+ if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') {
+ chart.showLegend(fd.show_legend);
+ }
+
+ var height = slice.height();
+ height -= 15; // accounting for the staggered xAxis
+
+ if (chart.hasOwnProperty("x2Axis")) {
+ height += 30;
+ }
+ chart.height(height);
+ slice.container.css('height', height + 'px');
+
+ if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) {
+ chart.useInteractiveGuideline(true);
+ }
+ if (fd.y_axis_zero) {
+ chart.forceY([0, 1]);
+ } else if (fd.y_log_scale) {
+ chart.yScale(d3.scale.log());
+ }
+ if (fd.x_log_scale) {
+ chart.xScale(d3.scale.log());
+ }
+ if (viz_type === 'bubble') {
+ chart.xAxis.tickFormat(d3.format('.3s'));
+ } else if (fd.x_axis_format === 'smart_date') {
+ chart.xAxis.tickFormat(px.formatDate);
+ } else if (fd.x_axis_format !== undefined) {
+ chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format));
+ }
+ if (chart.yAxis !== undefined) {
+ chart.yAxis.tickFormat(d3.format('.3s'));
+ }
+
+ if (fd.contribution || fd.num_period_compare || viz_type === 'compare') {
+ chart.yAxis.tickFormat(d3.format('.3p'));
+ if (chart.y2Axis !== undefined) {
+ chart.y2Axis.tickFormat(d3.format('.3p'));
+ }
+ } else if (fd.y_axis_format) {
+ chart.yAxis.tickFormat(d3.format(fd.y_axis_format));
+
+ if (chart.y2Axis !== undefined) {
+ chart.y2Axis.tickFormat(d3.format(fd.y_axis_format));
+ }
+ }
+
+ chart.color(function (d, i) {
+ return px.color.category21(d[colorKey]);
+ });
+
+ d3.select(slice.selector).html('');
+ d3.select(slice.selector).append("svg")
+ .datum(payload.data)
+ .transition().duration(500)
+ .attr('height', height)
+ .call(chart);
+
+ return chart;
+ });
+
+ slice.done(payload);
+ })
+ .fail(function (xhr) {
+ slice.error(xhr.responseText);
+ });
+ };
+
+ var update = function () {
+ if (chart && chart.update) {
+ chart.update();
+ }
+ };
+
+ return {
+ render: render,
+ resize: update
+ };
+}
+
+module.exports = nvd3Vis;
diff --git a/dashed/assets/visualizations/parallel_coordinates.js b/dashed/assets/visualizations/parallel_coordinates.js
new file mode 100644
index 000000000..271989b9f
--- /dev/null
+++ b/dashed/assets/visualizations/parallel_coordinates.js
@@ -0,0 +1,92 @@
+// JS
+var $ = window.$ || require('jquery');
+var d3 = window.d3 || require('d3');
+d3.parcoords = require('../vendor/parallel_coordinates/d3.parcoords.js');
+d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js');
+
+// CSS
+require('../vendor/parallel_coordinates/d3.parcoords.css');
+
+function parallelCoordVis(slice) {
+
+ function refresh() {
+ $('#code').attr('rows', '15');
+ $.getJSON(slice.jsonEndpoint(), function (payload) {
+ var data = payload.data;
+ var fd = payload.form_data;
+ var ext = d3.extent(data, function (d) {
+ return d[fd.secondary_metric];
+ });
+ ext = [ext[0], (ext[1] - ext[0]) / 2, ext[1]];
+ var cScale = d3.scale.linear()
+ .domain(ext)
+ .range(['red', 'grey', 'blue'])
+ .interpolate(d3.interpolateLab);
+
+ var color = function (d) {
+ return cScale(d[fd.secondary_metric]);
+ };
+ var container = d3.select(slice.selector);
+ var eff_height = fd.show_datatable ? (slice.height() / 2) : slice.height();
+
+ container.append('div')
+ .attr('id', 'parcoords_' + slice.container_id)
+ .style('height', eff_height + 'px')
+ .classed("parcoords", true);
+
+ var parcoords = d3.parcoords()('#parcoords_' + slice.container_id)
+ .width(slice.width())
+ .color(color)
+ .alpha(0.5)
+ .composite("darken")
+ .height(eff_height)
+ .data(payload.data)
+ .render()
+ .createAxes()
+ .shadows()
+ .reorderable()
+ .brushMode("1D-axes");
+
+ if (fd.show_datatable) {
+ // create data table, row hover highlighting
+ var grid = d3.divgrid();
+ container.append("div")
+ .datum(data.slice(0, 10))
+ .attr('id', "grid")
+ .call(grid)
+ .classed("parcoords", true)
+ .selectAll(".row")
+ .on({
+ mouseover: function (d) {
+ parcoords.highlight([d]);
+ },
+ mouseout: parcoords.unhighlight
+ });
+ // update data table on brush event
+ parcoords.on("brush", function (d) {
+ d3.select("#grid")
+ .datum(d.slice(0, 10))
+ .call(grid)
+ .selectAll(".row")
+ .on({
+ mouseover: function (d) {
+ parcoords.highlight([d]);
+ },
+ mouseout: parcoords.unhighlight
+ });
+ });
+ }
+ slice.done();
+ })
+ .fail(function (xhr) {
+ slice.error(xhr.responseText);
+ });
+ }
+
+ return {
+ render: refresh,
+ resize: refresh
+ };
+}
+
+module.exports = parallelCoordVis;
diff --git a/dashed/assets/visualizations/pivot_table.css b/dashed/assets/visualizations/pivot_table.css
new file mode 100644
index 000000000..8aef19a85
--- /dev/null
+++ b/dashed/assets/visualizations/pivot_table.css
@@ -0,0 +1,13 @@
+.gridster .widget.pivot_table {
+ overflow: auto !important;
+}
+
+.table tr>th {
+ padding: 1px 5px !important;
+ font-size: small !important;
+}
+
+.table tr>td {
+ padding: 1px 5px !important;
+ font-size: small !important;
+}
diff --git a/dashed/assets/visualizations/pivot_table.js b/dashed/assets/visualizations/pivot_table.js
new file mode 100644
index 000000000..795adbb29
--- /dev/null
+++ b/dashed/assets/visualizations/pivot_table.js
@@ -0,0 +1,31 @@
+var $ = window.$ = require('jquery');
+var jQuery = window.jQuery = $;
+
+require('datatables');
+require('./pivot_table.css');
+require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
+
+module.exports = function (slice) {
+ var container = slice.container;
+ var form_data = slice.data.form_data;
+
+ function refresh() {
+ $.getJSON(slice.jsonEndpoint(), function (json) {
+ container.html(json.data);
+ if (form_data.groupby.length === 1) {
+ var table = container.find('table').DataTable({
+ paging: false,
+ searching: false
+ });
+ table.column('-1').order('desc').draw();
+ }
+ slice.done(json);
+ }).fail(function (xhr) {
+ slice.error(xhr.responseText);
+ });
+ }
+ return {
+ render: refresh,
+ resize: refresh
+ };
+};
diff --git a/dashed/assets/visualizations/sankey.css b/dashed/assets/visualizations/sankey.css
new file mode 100644
index 000000000..9a2a0c88a
--- /dev/null
+++ b/dashed/assets/visualizations/sankey.css
@@ -0,0 +1,20 @@
+.sankey .node rect {
+ cursor: move;
+ fill-opacity: .9;
+ shape-rendering: crispEdges;
+}
+
+.sankey .node text {
+ pointer-events: none;
+ text-shadow: 0 1px 0 #fff;
+}
+
+.sankey .link {
+ fill: none;
+ stroke: #000;
+ stroke-opacity: .2;
+}
+
+.sankey .link:hover {
+ stroke-opacity: .5;
+}
diff --git a/dashed/assets/visualizations/sankey.js b/dashed/assets/visualizations/sankey.js
new file mode 100644
index 000000000..018ffe57d
--- /dev/null
+++ b/dashed/assets/visualizations/sankey.js
@@ -0,0 +1,140 @@
+// CSS
+require('./sankey.css');
+// JS
+var px = window.px || require('../javascripts/modules/dashed.js');
+var d3 = window.d3 || require('d3');
+d3.sankey = require('d3-sankey').sankey;
+
+function sankeyVis(slice) {
+ var div = d3.select(slice.selector);
+
+ var render = function () {
+ var margin = {
+ top: 5,
+ right: 5,
+ bottom: 5,
+ left: 5
+ };
+ var width = slice.width() - margin.left - margin.right;
+ var height = slice.height() - margin.top - margin.bottom;
+
+ var formatNumber = d3.format(",.0f"),
+ format = function (d) {
+ return formatNumber(d) + " TWh";
+ };
+
+ var svg = div.append("svg")
+ .attr("width", width + margin.left + margin.right)
+ .attr("height", height + margin.top + margin.bottom)
+ .append("g")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+ var sankey = d3.sankey()
+ .nodeWidth(15)
+ .nodePadding(10)
+ .size([width, height]);
+
+ var path = sankey.link();
+
+ d3.json(slice.jsonEndpoint(), function (error, json) {
+ if (error !== null) {
+ slice.error(error.responseText);
+ return '';
+ }
+ var links = json.data;
+ var nodes = {};
+ // Compute the distinct nodes from the links.
+ links.forEach(function (link) {
+ link.source = nodes[link.source] || (nodes[link.source] = { name: link.source });
+ link.target = nodes[link.target] || (nodes[link.target] = { name: link.target });
+ link.value = Number(link.value);
+ });
+ nodes = d3.values(nodes);
+
+ sankey
+ .nodes(nodes)
+ .links(links)
+ .layout(32);
+
+ var link = svg.append("g").selectAll(".link")
+ .data(links)
+ .enter().append("path")
+ .attr("class", "link")
+ .attr("d", path)
+ .style("stroke-width", function (d) {
+ return Math.max(1, d.dy);
+ })
+ .sort(function (a, b) {
+ return b.dy - a.dy;
+ });
+
+ link.append("title")
+ .text(function (d) {
+ return d.source.name + " → " + d.target.name + "\n" + format(d.value);
+ });
+
+ var node = svg.append("g").selectAll(".node")
+ .data(nodes)
+ .enter().append("g")
+ .attr("class", "node")
+ .attr("transform", function (d) {
+ return "translate(" + d.x + "," + d.y + ")";
+ })
+ .call(d3.behavior.drag()
+ .origin(function (d) {
+ return d;
+ })
+ .on("dragstart", function () {
+ this.parentNode.appendChild(this);
+ })
+ .on("drag", dragmove));
+
+ node.append("rect")
+ .attr("height", function (d) {
+ return d.dy;
+ })
+ .attr("width", sankey.nodeWidth())
+ .style("fill", function (d) {
+ d.color = px.color.category21(d.name.replace(/ .*/, ""));
+ return d.color;
+ })
+ .style("stroke", function (d) {
+ return d3.rgb(d.color).darker(2);
+ })
+ .append("title")
+ .text(function (d) {
+ return d.name + "\n" + format(d.value);
+ });
+
+ node.append("text")
+ .attr("x", -6)
+ .attr("y", function (d) {
+ return d.dy / 2;
+ })
+ .attr("dy", ".35em")
+ .attr("text-anchor", "end")
+ .attr("transform", null)
+ .text(function (d) {
+ return d.name;
+ })
+ .filter(function (d) {
+ return d.x < width / 2;
+ })
+ .attr("x", 6 + sankey.nodeWidth())
+ .attr("text-anchor", "start");
+
+ function dragmove(d) {
+ d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")");
+ sankey.relayout();
+ link.attr("d", path);
+ }
+ slice.done(json);
+ });
+ };
+ return {
+ render: render,
+ resize: render
+ };
+}
+
+module.exports = sankeyVis;
diff --git a/dashed/assets/visualizations/sunburst.css b/dashed/assets/visualizations/sunburst.css
new file mode 100644
index 000000000..d2636faaa
--- /dev/null
+++ b/dashed/assets/visualizations/sunburst.css
@@ -0,0 +1,39 @@
+.sunburst text {
+ shape-rendering: crispEdges;
+}
+.sunburst path {
+ stroke: #333;
+ stroke-width: 0.5px;
+}
+.sunburst .center-label {
+ text-anchor: middle;
+ fill: #000;
+ pointer-events: none;
+}
+.sunburst .path-percent {
+ font-size: 4em;
+}
+.sunburst .path-metrics {
+ font-size: 1.75em;
+}
+.sunburst .path-ratio {
+ font-size: 1.2em;
+}
+
+.sunburst .breadcrumbs text {
+ font-weight: 600;
+ font-size: 1.2em;
+ text-anchor: middle;
+ fill: #000;
+}
+
+/* dashboard specific */
+.dashboard .sunburst text {
+ font-size: 1em;
+}
+.dashboard .sunburst .path-percent {
+ font-size: 2.5em;
+}
+.dashboard .sunburst .path-metrics {
+ font-size: 1em;
+}
diff --git a/dashed/assets/visualizations/sunburst.js b/dashed/assets/visualizations/sunburst.js
new file mode 100644
index 000000000..e195e71af
--- /dev/null
+++ b/dashed/assets/visualizations/sunburst.js
@@ -0,0 +1,359 @@
+var d3 = window.d3 || require('d3');
+var px = require('../javascripts/modules/dashed.js');
+var wrapSvgText = require('../javascripts/modules/utils.js').wrapSvgText;
+
+require('./sunburst.css');
+
+// Modified from http://bl.ocks.org/kerryrodden/7090426
+function sunburstVis(slice) {
+ var container = d3.select(slice.selector);
+
+ var render = function () {
+ // vars with shared scope within this function
+ var margin = { top: 10, right: 5, bottom: 10, left: 5 };
+ var containerWidth = slice.width();
+ var containerHeight = slice.height();
+ var breadcrumbHeight = containerHeight * 0.085;
+ var visWidth = containerWidth - margin.left - margin.right;
+ var visHeight = containerHeight - margin.top - margin.bottom - breadcrumbHeight;
+ var radius = Math.min(visWidth, visHeight) / 2;
+ var colorByCategory = true; // color by category if primary/secondary metrics match
+
+ var maxBreadcrumbs, breadcrumbDims, // set based on data
+ totalSize, // total size of all segments; set after loading the data.
+ colorScale,
+ breadcrumbs, vis, arcs, gMiddleText; // dom handles
+
+ // Helper + path gen functions
+ var partition = d3.layout.partition()
+ .size([2 * Math.PI, radius * radius])
+ .value(function (d) { return d.m1; });
+
+ var arc = d3.svg.arc()
+ .startAngle(function (d) {
+ return d.x;
+ })
+ .endAngle(function (d) {
+ return d.x + d.dx;
+ })
+ .innerRadius(function (d) {
+ return Math.sqrt(d.y);
+ })
+ .outerRadius(function (d) {
+ return Math.sqrt(d.y + d.dy);
+ });
+
+ var f = d3.format(".3s");
+ var fp = d3.format(".3p");
+
+ container.select("svg").remove();
+
+ var svg = container.append("svg:svg")
+ .attr("width", containerWidth)
+ .attr("height", containerHeight);
+
+ d3.json(slice.jsonEndpoint(), function (error, rawData) {
+ if (error !== null) {
+ slice.error(error.responseText);
+ return '';
+ }
+
+ createBreadcrumbs(rawData);
+ createVisualization(rawData);
+
+ slice.done(rawData);
+ });
+
+ function createBreadcrumbs(rawData) {
+ var firstRowData = rawData.data[0];
+ maxBreadcrumbs = (firstRowData.length - 2) + 1; // -2 bc row contains 2x metrics, +extra for %label and buffer
+
+ breadcrumbDims = {
+ width: visWidth / maxBreadcrumbs,
+ height: breadcrumbHeight *0.8, // more margin
+ spacing: 3,
+ tipTailWidth: 10
+ };
+
+ breadcrumbs = svg.append("svg:g")
+ .attr("class", "breadcrumbs")
+ .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+ breadcrumbs.append("svg:text")
+ .attr("class", "end-label");
+ }
+
+ // Main function to draw and set up the visualization, once we have the data.
+ function createVisualization(rawData) {
+ var tree = buildHierarchy(rawData.data);
+
+ vis = svg.append("svg:g")
+ .attr("class", "sunburst-vis")
+ .attr("transform", "translate(" + (margin.left + (visWidth / 2)) + "," + (margin.top + breadcrumbHeight + (visHeight / 2)) + ")")
+ .on("mouseleave", mouseleave);
+
+ arcs = vis.append("svg:g")
+ .attr("id", "arcs");
+
+ gMiddleText = vis.append("svg:g")
+ .attr("class", "center-label");
+
+ // Bounding circle underneath the sunburst, to make it easier to detect
+ // when the mouse leaves the parent g.
+ arcs.append("svg:circle")
+ .attr("r", radius)
+ .style("opacity", 0);
+
+ // For efficiency, filter nodes to keep only those large enough to see.
+ var nodes = partition.nodes(tree)
+ .filter(function (d) {
+ return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
+ });
+
+ var ext;
+
+ if (rawData.form_data.metric !== rawData.form_data.secondary_metric) {
+ colorByCategory = false;
+
+ ext = d3.extent(nodes, function (d) {
+ return d.m2 / d.m1;
+ });
+
+ colorScale = d3.scale.linear()
+ .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]])
+ .range(["#00D1C1", "white", "#FFB400"]);
+ }
+
+ var path = arcs.data([tree]).selectAll("path")
+ .data(nodes)
+ .enter().append("svg:path")
+ .attr("display", function (d) {
+ return d.depth ? null : "none";
+ })
+ .attr("d", arc)
+ .attr("fill-rule", "evenodd")
+ .style("fill", function (d) {
+ return colorByCategory ? px.color.category21(d.name) : colorScale(d.m2 / d.m1);
+ })
+ .style("opacity", 1)
+ .on("mouseenter", mouseenter);
+
+ // Get total size of the tree = value of root node from partition.
+ totalSize = path.node().__data__.value;
+ }
+
+ // Fade all but the current sequence, and show it in the breadcrumb trail.
+ function mouseenter(d) {
+
+ var percentage = (d.m1 / totalSize).toPrecision(3);
+ var percentageString = fp(percentage);
+ var metricsMatch = Math.abs(d.m1 - d.m2) < 0.000001;
+
+ gMiddleText.selectAll("*").remove();
+
+ gMiddleText.append("text")
+ .attr("class", "path-percent")
+ .attr("y", "-10")
+ .text(percentageString);
+
+ gMiddleText.append("text")
+ .attr("class", "path-metrics")
+ .attr("y", "25")
+ .text("m1: " + f(d.m1) + (metricsMatch ? "" : ", m2: " + f(d.m2)));
+
+ gMiddleText.append("text")
+ .attr("class", "path-ratio")
+ .attr("y", "50")
+ .text("m2/m1: " + fp(d.m2 / d.m1));
+
+ var sequenceArray = getAncestors(d);
+
+ // Reset and fade all the segments.
+ arcs.selectAll("path")
+ .style("stroke-width", null)
+ .style("stroke", null)
+ .style("opacity", 0.3);
+
+ // Then highlight only those that are an ancestor of the current segment.
+ arcs.selectAll("path")
+ .filter(function (node) {
+ return (sequenceArray.indexOf(node) >= 0);
+ })
+ .style("opacity", 1)
+ .style("stroke-width", "2px")
+ .style("stroke", "#000");
+
+ updateBreadcrumbs(sequenceArray, percentageString);
+ }
+
+ // Restore everything to full opacity when moving off the visualization.
+ function mouseleave(d) {
+
+ // Hide the breadcrumb trail
+ breadcrumbs.style("visibility", "hidden");
+
+ gMiddleText.selectAll("*").remove();
+
+ // Deactivate all segments during transition.
+ arcs.selectAll("path").on("mouseenter", null);
+ //gMiddleText.selectAll("*").remove();
+
+ // Transition each segment to full opacity and then reactivate it.
+ arcs.selectAll("path")
+ .transition()
+ .duration(200)
+ .style("opacity", 1)
+ .style("stroke", null)
+ .style("stroke-width", null)
+ .each("end", function () {
+ d3.select(this).on("mouseenter", mouseenter);
+ });
+ }
+
+ // Given a node in a partition layout, return an array of all of its ancestor
+ // nodes, highest first, but excluding the root.
+ function getAncestors(node) {
+ var path = [];
+ var current = node;
+ while (current.parent) {
+ path.unshift(current);
+ current = current.parent;
+ }
+ return path;
+ }
+
+ // Generate a string that describes the points of a breadcrumb polygon.
+ function breadcrumbPoints(d, i) {
+ var points = [];
+ points.push("0,0");
+ points.push(breadcrumbDims.width + ",0");
+ points.push(breadcrumbDims.width + breadcrumbDims.tipTailWidth + "," + (breadcrumbDims.height / 2));
+ points.push(breadcrumbDims.width+ "," + breadcrumbDims.height);
+ points.push("0," + breadcrumbDims.height);
+ if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
+ points.push(breadcrumbDims.tipTailWidth + "," + (breadcrumbDims.height / 2));
+ }
+ return points.join(" ");
+ }
+
+ function updateBreadcrumbs(sequenceArray, percentageString) {
+ var g = breadcrumbs.selectAll("g")
+ .data(sequenceArray, function (d) {
+ return d.name + d.depth;
+ });
+
+ // Add breadcrumb and label for entering nodes.
+ var entering = g.enter().append("svg:g");
+
+ entering.append("svg:polygon")
+ .attr("points", breadcrumbPoints)
+ .style("fill", function (d) {
+ return colorByCategory ? px.color.category21(d.name) : colorScale(d.m2 / d.m1);
+ });
+
+ entering.append("svg:text")
+ .attr("x", (breadcrumbDims.width + breadcrumbDims.tipTailWidth) / 2)
+ .attr("y", breadcrumbDims.height / 4)
+ .attr("dy", "0.35em")
+ .attr("class", "step-label")
+ .text(function (d) { return d.name; })
+ .call(wrapSvgText, breadcrumbDims.width, breadcrumbDims.height / 2);
+
+ // Set position for entering and updating nodes.
+ g.attr("transform", function (d, i) {
+ return "translate(" + i * (breadcrumbDims.width + breadcrumbDims.spacing) + ", 0)";
+ });
+
+ // Remove exiting nodes.
+ g.exit().remove();
+
+ // Now move and update the percentage at the end.
+ breadcrumbs.select(".end-label")
+ .attr("x", (sequenceArray.length + 0.5) * (breadcrumbDims.width + breadcrumbDims.spacing))
+ .attr("y", breadcrumbDims.height / 2)
+ .attr("dy", "0.35em")
+ .text(percentageString);
+
+ // Make the breadcrumb trail visible, if it's hidden.
+ breadcrumbs.style("visibility", null);
+ }
+
+ function buildHierarchy(rows) {
+ var root = {
+ name: "root",
+ children: []
+ };
+ for (var i = 0; i < rows.length; i++) {
+ var row = rows[i];
+ var m1 = Number(row[row.length - 2]);
+ var m2 = Number(row[row.length - 1]);
+ var levels = row.slice(0, row.length - 2);
+ if (isNaN(m1)) { // e.g. if this is a header row
+ continue;
+ }
+ var currentNode = root;
+ for (var j = 0; j < levels.length; j++) {
+ var children = currentNode.children;
+ var nodeName = levels[j];
+ // If the next node has the name "0", it will
+ var isLeafNode = (j >= levels.length - 1) || levels[j+1] === 0;
+ var childNode;
+
+ if (!isLeafNode) {
+ // Not yet at the end of the sequence; move down the tree.
+ var foundChild = false;
+ for (var k = 0; k < children.length; k++) {
+ if (children[k].name === nodeName) {
+ childNode = children[k];
+ foundChild = true;
+ break;
+ }
+ }
+ // If we don't already have a child node for this branch, create it.
+ if (!foundChild) {
+ childNode = {
+ name: nodeName,
+ children: []
+ };
+ children.push(childNode);
+ }
+ currentNode = childNode;
+ } else if (nodeName !== 0) {
+ // Reached the end of the sequence; create a leaf node.
+ childNode = {
+ name: nodeName,
+ m1: m1,
+ m2: m2
+ };
+ children.push(childNode);
+ }
+ }
+ }
+
+ function recurse(node) {
+ if (node.children) {
+ var sums;
+ var m1 = 0;
+ var m2 = 0;
+ for (var i = 0; i < node.children.length; i++) {
+ sums = recurse(node.children[i]);
+ m1 += sums[0];
+ m2 += sums[1];
+ }
+ node.m1 = m1;
+ node.m2 = m2;
+ }
+ return [node.m1, node.m2];
+ }
+ recurse(root);
+ return root;
+ }
+ };
+
+ return {
+ render: render,
+ resize: render
+ };
+}
+
+module.exports = sunburstVis;
diff --git a/dashed/assets/visualizations/table.css b/dashed/assets/visualizations/table.css
new file mode 100644
index 000000000..cd70e14eb
--- /dev/null
+++ b/dashed/assets/visualizations/table.css
@@ -0,0 +1,18 @@
+.gridster .widget.table {
+ overflow: auto !important;
+}
+
+.widget.table td.filtered {
+ background-color: #005a63;
+ color: white;
+}
+
+.table tr>th {
+ padding: 1px 5px !important;
+ font-size: small !important;
+}
+
+.table tr>td {
+ padding: 1px 5px !important;
+ font-size: small !important;
+}
diff --git a/dashed/assets/visualizations/table.js b/dashed/assets/visualizations/table.js
new file mode 100644
index 000000000..937b0769b
--- /dev/null
+++ b/dashed/assets/visualizations/table.js
@@ -0,0 +1,124 @@
+var $ = window.$ = require('jquery');
+var jQuery = window.jQuery = $;
+var d3 = require('d3');
+
+require('./table.css');
+require('datatables');
+require('../node_modules/datatables-bootstrap3-plugin/media/css/datatables-bootstrap3.css');
+
+function tableVis(slice) {
+ var data = slice.data;
+ var form_data = data.form_data;
+ var f = d3.format('.3s');
+ var fC = d3.format('0,000');
+
+ function refresh() {
+ $.getJSON(slice.jsonEndpoint(), onSuccess).fail(onError);
+
+ function onError(xhr) {
+ slice.error(xhr.responseText);
+ }
+
+ function onSuccess(json) {
+ var data = json.data;
+ var metrics = json.form_data.metrics;
+
+ function col(c) {
+ var arr = [];
+ for (var i = 0; i < data.records.length; i++) {
+ arr.push(json.data.records[i][c]);
+ }
+ return arr;
+ }
+ var maxes = {};
+ for (var i = 0; i < metrics.length; i++) {
+ maxes[metrics[i]] = d3.max(col(metrics[i]));
+ }
+
+ var table = d3.select(slice.selector).append('table')
+ .classed('dataframe dataframe table table-striped table-bordered table-condensed table-hover dataTable no-footer', true);
+
+ table.append('thead').append('tr')
+ .selectAll('th')
+ .data(data.columns).enter()
+ .append('th')
+ .text(function (d) {
+ return d;
+ });
+
+ table.append('tbody')
+ .selectAll('tr')
+ .data(data.records).enter()
+ .append('tr')
+ .selectAll('td')
+ .data(function (row, i) {
+ return data.columns.map(function (c) {
+ return {
+ col: c,
+ val: row[c],
+ isMetric: metrics.indexOf(c) >= 0
+ };
+ });
+ }).enter()
+ .append('td')
+ .style('background-image', function (d) {
+ if (d.isMetric) {
+ var perc = Math.round((d.val / maxes[d.col]) * 100);
+ return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%";
+ }
+ })
+ .attr('title', function (d) {
+ if (!isNaN(d.val)) {
+ return fC(d.val);
+ }
+ })
+ .attr('data-sort', function (d) {
+ if (d.isMetric) {
+ return d.val;
+ }
+ })
+ .on("click", function (d) {
+ if (!d.isMetric) {
+ var td = d3.select(this);
+ if (td.classed('filtered')) {
+ slice.removeFilter(d.col, [d.val]);
+ d3.select(this).classed('filtered', false);
+ } else {
+ d3.select(this).classed('filtered', true);
+ slice.addFilter(d.col, [d.val]);
+ }
+ }
+ })
+ .style("cursor", function (d) {
+ if (!d.isMetric) {
+ return 'pointer';
+ }
+ })
+ .html(function (d) {
+ if (d.isMetric) {
+ return f(d.val);
+ } else {
+ return d.val;
+ }
+ });
+ var datatable = slice.container.find('.dataTable').DataTable({
+ paging: false,
+ searching: form_data.include_search
+ });
+ // Sorting table by main column
+ if (form_data.metrics.length > 0) {
+ var main_metric = form_data.metrics[0];
+ datatable.column(data.columns.indexOf(main_metric)).order('desc').draw();
+ }
+ slice.done(json);
+ slice.container.parents('.widget').find('.tooltip').remove();
+ }
+ }
+
+ return {
+ render: refresh,
+ resize: function () {}
+ };
+}
+
+module.exports = tableVis;
diff --git a/dashed/assets/visualizations/word_cloud.js b/dashed/assets/visualizations/word_cloud.js
new file mode 100644
index 000000000..503807a7e
--- /dev/null
+++ b/dashed/assets/visualizations/word_cloud.js
@@ -0,0 +1,91 @@
+var px = window.px || require('../javascripts/modules/dashed.js');
+var d3 = window.d3 || require('d3');
+var cloudLayout = require('d3-cloud');
+
+function wordCloudChart(slice) {
+ var chart = d3.select(slice.selector);
+
+ function refresh() {
+ d3.json(slice.jsonEndpoint(), function (error, json) {
+ if (error !== null) {
+ slice.error(error.responseText);
+ return '';
+ }
+ var data = json.data;
+ var range = [
+ json.form_data.size_from,
+ json.form_data.size_to
+ ];
+ var rotation = json.form_data.rotation;
+ var f_rotation;
+ if (rotation === "square") {
+ f_rotation = function () {
+ return ~~(Math.random() * 2) * 90;
+ };
+ } else if (rotation === "flat") {
+ f_rotation = function () {
+ return 0;
+ };
+ } else {
+ f_rotation = function () {
+ return (~~(Math.random() * 6) - 3) * 30;
+ };
+ }
+ var size = [slice.width(), slice.height()];
+
+ var scale = d3.scale.linear()
+ .range(range)
+ .domain(d3.extent(data, function (d) {
+ return d.size;
+ }));
+
+ var layout = cloudLayout()
+ .size(size)
+ .words(data)
+ .padding(5)
+ .rotate(f_rotation)
+ .font("serif")
+ .fontSize(function (d) {
+ return scale(d.size);
+ })
+ .on("end", draw);
+
+ layout.start();
+
+ function draw(words) {
+ chart.selectAll("*").remove();
+
+ chart.append("svg")
+ .attr("width", layout.size()[0])
+ .attr("height", layout.size()[1])
+ .append("g")
+ .attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")")
+ .selectAll("text")
+ .data(words)
+ .enter().append("text")
+ .style("font-size", function (d) {
+ return d.size + "px";
+ })
+ .style("font-family", "Impact")
+ .style("fill", function (d) {
+ return px.color.category21(d.text);
+ })
+ .attr("text-anchor", "middle")
+ .attr("transform", function (d) {
+ return "translate(" + [d.x, d.y] + ") rotate(" + d.rotate + ")";
+ })
+ .text(function (d) {
+ return d.text;
+ });
+ }
+ slice.done(data);
+ });
+ }
+
+ return {
+ render: refresh,
+ resize: refresh
+ };
+}
+
+module.exports = wordCloudChart;
diff --git a/dashed/assets/visualizations/world_map.css b/dashed/assets/visualizations/world_map.css
new file mode 100644
index 000000000..99e1bf6f0
--- /dev/null
+++ b/dashed/assets/visualizations/world_map.css
@@ -0,0 +1,7 @@
+.world_map svg {
+ background-color: #feffff;
+}
+
+.world_map {
+ position: relative;
+}
diff --git a/dashed/assets/visualizations/world_map.js b/dashed/assets/visualizations/world_map.js
new file mode 100644
index 000000000..87f09b30b
--- /dev/null
+++ b/dashed/assets/visualizations/world_map.js
@@ -0,0 +1,110 @@
+// JS
+var d3 = window.d3 || require('d3');
+//var Datamap = require('../vendor/datamaps/datamaps.all.js');
+var Datamap = require('datamaps');
+
+// CSS
+require('./world_map.css');
+
+function worldMapChart(slice) {
+ var render = function () {
+ var container = slice.container;
+ var div = d3.select(slice.selector);
+
+ container.css('height', slice.height());
+
+ d3.json(slice.jsonEndpoint(), function (error, json) {
+ var fd = json.form_data;
+
+ if (error !== null) {
+ slice.error(error.responseText);
+ return '';
+ }
+ var ext = d3.extent(json.data, function (d) {
+ return d.m1;
+ });
+ var extRadius = d3.extent(json.data, function (d) {
+ return d.m2;
+ });
+ var radiusScale = d3.scale.linear()
+ .domain([extRadius[0], extRadius[1]])
+ .range([1, fd.max_bubble_size]);
+
+ json.data.forEach(function (d) {
+ d.radius = radiusScale(d.m2);
+ });
+
+ var colorScale = d3.scale.linear()
+ .domain([ext[0], ext[1]])
+ .range(["#FFF", "black"]);
+
+ var d = {};
+ for (var i = 0; i < json.data.length; i++) {
+ var country = json.data[i];
+ country.fillColor = colorScale(country.m1);
+ d[country.country] = country;
+ }
+
+ var f = d3.format('.3s');
+
+ container.show();
+
+ var map = new Datamap({
+ element: slice.container.get(0),
+ data: json.data,
+ fills: {
+ defaultFill: '#ddd'
+ },
+ geographyConfig: {
+ popupOnHover: true,
+ highlightOnHover: true,
+ borderWidth: 1,
+ borderColor: '#fff',
+ highlightBorderColor: '#fff',
+ highlightFillColor: '#005a63',
+ highlightBorderWidth: 1,
+ popupTemplate: function (geo, data) {
+ return '
' + data.name + ' ' + f(data.m1) + '
';
+ }
+ },
+ bubblesConfig: {
+ borderWidth: 1,
+ borderOpacity: 1,
+ borderColor: '#005a63',
+ popupOnHover: true,
+ radius: null,
+ popupTemplate: function (geo, data) {
+ return '
' + data.name + ' ' + f(data.m2) + '
';
+ },
+ fillOpacity: 0.5,
+ animate: true,
+ highlightOnHover: true,
+ highlightFillColor: '#005a63',
+ highlightBorderColor: 'black',
+ highlightBorderWidth: 2,
+ highlightBorderOpacity: 1,
+ highlightFillOpacity: 0.85,
+ exitDelay: 100,
+ key: JSON.stringify
+ }
+ });
+
+ map.updateChoropleth(d);
+
+ if (fd.show_bubbles) {
+ map.bubbles(json.data);
+ div.selectAll("circle.datamaps-bubble").style('fill', '#005a63');
+ }
+
+ slice.done(json);
+
+ });
+ };
+
+ return {
+ render: render,
+ resize: render
+ };
+}
+
+module.exports = worldMapChart;
diff --git a/dashed/assets/webpack.config.js b/dashed/assets/webpack.config.js
new file mode 100644
index 000000000..465a04e1c
--- /dev/null
+++ b/dashed/assets/webpack.config.js
@@ -0,0 +1,51 @@
+var path = require('path');
+var APP_DIR = path.resolve(__dirname, './'); // input
+var BUILD_DIR = path.resolve(__dirname, './javascripts/dist'); // output
+
+var config = {
+ // for now generate one compiled js file per entry point / html page
+ entry: {
+ 'css-theme': APP_DIR + '/javascripts/css-theme.js',
+ dashboard: APP_DIR + '/javascripts/dashboard.js',
+ explore: APP_DIR + '/javascripts/explore.js',
+ featured: APP_DIR + '/javascripts/featured.js',
+ sql: APP_DIR + '/javascripts/sql.js',
+ standalone: APP_DIR + '/javascripts/standalone.js'
+ },
+ output: {
+ path: BUILD_DIR,
+ filename: '[name].entry.js'
+ },
+ module: {
+ loaders: [
+ {
+ test: /\.jsx?/,
+ include: APP_DIR,
+ exclude: APP_DIR + '/node_modules',
+ loader: 'babel'
+ },
+ /* for require('*.css') */
+ {
+ test: /\.css$/,
+ include: APP_DIR,
+ loader: "style-loader!css-loader"
+ },
+ /* for css linking images */
+ { test: /\.png$/, loader: "url-loader?limit=100000" },
+ { test: /\.jpg$/, loader: "file-loader" },
+ { test: /\.gif$/, loader: "file-loader" },
+ /* for font-awesome */
+ { test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "url-loader?limit=10000&minetype=application/font-woff" },
+ { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: "file-loader" },
+ /* for require('*.less') */
+ {
+ test: /\.less$/,
+ include: APP_DIR,
+ loader: "style!css!less"
+ }
+ ]
+ },
+ plugins: []
+};
+
+module.exports = config;
diff --git a/dashed/bin/__init__.py b/dashed/bin/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/dashed/bin/dashed b/dashed/bin/dashed
new file mode 100755
index 000000000..ac47bb6e9
--- /dev/null
+++ b/dashed/bin/dashed
@@ -0,0 +1,91 @@
+#!/usr/bin/env python
+
+from datetime import datetime
+import logging
+from subprocess import Popen
+
+from flask.ext.script import Manager
+from dashed import app
+from flask.ext.migrate import MigrateCommand
+import dashed
+from dashed import db
+from dashed import data, utils
+
+config = app.config
+
+manager = Manager(app)
+manager.add_command('db', MigrateCommand)
+
+
+@manager.option(
+ '-d', '--debug', action='store_true',
+ help="Start the web server in debug mode")
+@manager.option(
+ '-p', '--port', default=config.get("DASHED_WEBSERVER_PORT"),
+ help="Specify the port on which to run the web server")
+@manager.option(
+ '-w', '--workers', default=config.get("DASHED_WORKERS", 16),
+ help="Number of gunicorn web server workers to fire up")
+@manager.option(
+ '-t', '--timeout', default=config.get("DASHED_WEBSERVER_TIMEOUT"),
+ help="Specify the timeout (seconds) for the gunicorn web server")
+def runserver(debug, port, timeout, workers):
+ """Starts a Dashed web server"""
+ debug = debug or config.get("DEBUG")
+ if debug:
+ app.run(
+ host='0.0.0.0',
+ port=int(port),
+ debug=True)
+ else:
+ cmd = (
+ "gunicorn "
+ "-w {workers} "
+ "--timeout {timeout} "
+ "-b 0.0.0.0:{port} "
+ "dashed:app").format(**locals())
+ print("Starting server with command: " + cmd)
+ Popen(cmd, shell=True).wait()
+
+@manager.command
+def init():
+ """Inits the Dashed application"""
+ utils.init(dashed)
+
+@manager.option(
+ '-s', '--sample', action='store_true',
+ help="Only load 1000 rows (faster, used for testing)")
+def load_examples(sample):
+ """Loads a set of Slices and Dashboards and a supporting dataset """
+ print("Loading examples into {}".format(db))
+
+ data.load_css_templates()
+
+ print("Loading [World Bank's Health Nutrition and Population Stats]")
+ data.load_world_bank_health_n_pop()
+
+ print("Loading [Birth names]")
+ data.load_birth_names()
+
+@manager.command
+def refresh_druid():
+ """Refresh all druid datasources"""
+ session = db.session()
+ from dashed import models
+ for cluster in session.query(models.DruidCluster).all():
+ try:
+ cluster.refresh_datasources()
+ except Exception as e:
+ print(
+ "Error while processing cluster '{}'\n{}".format(
+ cluster, str(e)))
+ logging.exception(e)
+ cluster.metadata_last_refreshed = datetime.now()
+ print(
+ "Refreshed metadata from cluster "
+ "[" + cluster.cluster_name + "]")
+ session.commit()
+
+
+if __name__ == "__main__":
+ manager.run()
diff --git a/dashed/config.py b/dashed/config.py
new file mode 100644
index 000000000..a6561114f
--- /dev/null
+++ b/dashed/config.py
@@ -0,0 +1,118 @@
+"""
+All configuration in this file can be overridden by providing a local_config
+in your PYTHONPATH.
+
+There' a ``from local_config import *`` at the end of this file.
+"""
+import os
+from flask_appbuilder.security.manager import AUTH_DB
+# from flask_appbuilder.security.manager import (
+# AUTH_OID, AUTH_REMOTE_USER, AUTH_DB, AUTH_LDAP, AUTH_OAUTH)
+BASE_DIR = os.path.abspath(os.path.dirname(__file__))
+from dateutil import tz
+
+
+# ---------------------------------------------------------
+# Dashed specifix config
+# ---------------------------------------------------------
+ROW_LIMIT = 50000
+WEBSERVER_THREADS = 8
+
+DASHED_WEBSERVER_PORT = 8088
+DASHED_WEBSERVER_TIMEOUT = 60
+
+CUSTOM_SECURITY_MANAGER = None
+# ---------------------------------------------------------
+
+# Your App secret key
+SECRET_KEY = '\2\1thisismyscretkey\1\2\e\y\y\h' # noqa
+
+# The SQLAlchemy connection string.
+SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dashed.db'
+# SQLALCHEMY_DATABASE_URI = 'mysql://myapp@localhost/myapp'
+# SQLALCHEMY_DATABASE_URI = 'postgresql://root:password@localhost/myapp'
+
+# Flask-WTF flag for CSRF
+CSRF_ENABLED = True
+
+# Whether to run the web server in debug mode or not
+DEBUG = True
+
+# Whether to show the stacktrace on 500 error
+SHOW_STACKTRACE = True
+
+# ------------------------------
+# GLOBALS FOR APP Builder
+# ------------------------------
+# Uncomment to setup Your App name
+APP_NAME = "Dashed"
+
+# Uncomment to setup Setup an App icon
+# APP_ICON = "/static/img/something.png"
+
+# Druid query timezone
+# tz.tzutc() : Using utc timezone
+# tz.tzlocal() : Using local timezone
+# other tz can be overridden by providing a local_config
+DRUID_TZ = tz.tzutc()
+
+# ----------------------------------------------------
+# AUTHENTICATION CONFIG
+# ----------------------------------------------------
+# The authentication type
+# AUTH_OID : Is for OpenID
+# AUTH_DB : Is for database (username/password()
+# AUTH_LDAP : Is for LDAP
+# AUTH_REMOTE_USER : Is for using REMOTE_USER from web server
+AUTH_TYPE = AUTH_DB
+
+# Uncomment to setup Full admin role name
+# AUTH_ROLE_ADMIN = 'Admin'
+
+# Uncomment to setup Public role name, no authentication needed
+# AUTH_ROLE_PUBLIC = 'Public'
+
+# Will allow user self registration
+# AUTH_USER_REGISTRATION = True
+
+# The default user self registration role
+# AUTH_USER_REGISTRATION_ROLE = "Public"
+
+# When using LDAP Auth, setup the ldap server
+# AUTH_LDAP_SERVER = "ldap://ldapserver.new"
+
+# Uncomment to setup OpenID providers example for OpenID authentication
+# OPENID_PROVIDERS = [
+# { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' },
+# { 'name': 'AOL', 'url': 'http://openid.aol.com/
' },
+# { 'name': 'Flickr', 'url': 'http://www.flickr.com/' },
+# { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }]
+# ---------------------------------------------------
+# Babel config for translations
+# ---------------------------------------------------
+# Setup default language
+BABEL_DEFAULT_LOCALE = 'en'
+# Your application default translation path
+BABEL_DEFAULT_FOLDER = 'translations'
+# The allowed translation for you app
+LANGUAGES = {
+ 'en': {'flag': 'us', 'name': 'English'},
+}
+# ---------------------------------------------------
+# Image and file configuration
+# ---------------------------------------------------
+# The file upload folder, when using models with files
+UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/'
+
+# The image upload folder, when using models with images
+IMG_UPLOAD_FOLDER = BASE_DIR + '/app/static/uploads/'
+
+# The image upload url, when using models with images
+IMG_UPLOAD_URL = '/static/uploads/'
+# Setup image size default is (300, 200, True)
+# IMG_SIZE = (300, 200, True)
+
+try:
+ from dashed_config import * # noqa
+except Exception:
+ pass
diff --git a/dashed/data/__init__.py b/dashed/data/__init__.py
new file mode 100644
index 000000000..b02339ad1
--- /dev/null
+++ b/dashed/data/__init__.py
@@ -0,0 +1,624 @@
+"""Loads datasets, dashboards and slices in a new dashed instance"""
+
+import gzip
+import json
+import os
+import textwrap
+
+import pandas as pd
+from sqlalchemy import String, DateTime
+
+from dashed import app, db, models, utils
+
+# Shortcuts
+DB = models.Database
+Slice = models.Slice
+TBL = models.SqlaTable
+Dash = models.Dashboard
+
+config = app.config
+
+DATA_FOLDER = os.path.join(config.get("BASE_DIR"), 'data')
+
+
+def get_or_create_db(session):
+ print("Creating database reference")
+ dbobj = session.query(DB).filter_by(database_name='main').first()
+ if not dbobj:
+ dbobj = DB(database_name="main")
+ print(config.get("SQLALCHEMY_DATABASE_URI"))
+ dbobj.sqlalchemy_uri = config.get("SQLALCHEMY_DATABASE_URI")
+ session.add(dbobj)
+ session.commit()
+ return dbobj
+
+
+def merge_slice(slc):
+ o = db.session.query(Slice).filter_by(slice_name=slc.slice_name).first()
+ if o:
+ db.session.delete(o)
+ db.session.add(slc)
+ db.session.commit()
+
+
+def get_slice_json(defaults, **kwargs):
+ d = defaults.copy()
+ d.update(kwargs)
+ return json.dumps(d, indent=4, sort_keys=True)
+
+
+def load_world_bank_health_n_pop():
+ """Loads the world bank health dataset, slices and a dashboard"""
+ tbl_name = 'wb_health_population'
+ with gzip.open(os.path.join(DATA_FOLDER, 'countries.json.gz')) as f:
+ pdf = pd.read_json(f)
+ pdf.columns = [col.replace('.', '_') for col in pdf.columns]
+ pdf.year = pd.to_datetime(pdf.year)
+ pdf.to_sql(
+ tbl_name,
+ db.engine,
+ if_exists='replace',
+ chunksize=500,
+ dtype={
+ 'year': DateTime(),
+ 'country_code': String(3),
+ 'country_name': String(255),
+ 'region': String(255),
+ },
+ index=False)
+
+ print("Creating table [wb_health_population] reference")
+ tbl = db.session.query(TBL).filter_by(table_name=tbl_name).first()
+ if not tbl:
+ tbl = TBL(table_name=tbl_name)
+ tbl.description = utils.readfile(os.path.join(DATA_FOLDER, 'countries.md'))
+ tbl.main_dttm_col = 'year'
+ tbl.is_featured = True
+ tbl.database = get_or_create_db(db.session)
+ db.session.merge(tbl)
+ db.session.commit()
+ tbl.fetch_metadata()
+
+ defaults = {
+ "compare_lag": "10",
+ "compare_suffix": "o10Y",
+ "datasource_id": "1",
+ "datasource_name": "birth_names",
+ "datasource_type": "table",
+ "limit": "25",
+ "granularity": "year",
+ "groupby": [],
+ "metric": 'sum__SP_POP_TOTL',
+ "metrics": ["sum__SP_POP_TOTL"],
+ "row_limit": config.get("ROW_LIMIT"),
+ "since": "2014-01-01",
+ "until": "2014-01-01",
+ "where": "",
+ "markup_type": "markdown",
+ "country_fieldtype": "cca3",
+ "secondary_metric": "sum__SP_POP_TOTL",
+ "entity": "country_code",
+ "show_bubbles": "y",
+ }
+
+ print("Creating slices")
+ slices = [
+ Slice(
+ slice_name="Region Filter",
+ viz_type='filter_box',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type='filter_box',
+ groupby=['region'],
+ )),
+ Slice(
+ slice_name="World's Population",
+ viz_type='big_number',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ since='2000',
+ viz_type='big_number',
+ compare_lag="10",
+ metric='sum__SP_POP_TOTL',
+ compare_suffix="over 10Y")),
+ Slice(
+ slice_name="Most Populated Countries",
+ viz_type='table',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type='table',
+ metrics=["sum__SP_POP_TOTL"],
+ groupby=['country_name'])),
+ Slice(
+ slice_name="Growth Rate",
+ viz_type='line',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type='line',
+ since="1960-01-01",
+ metrics=["sum__SP_POP_TOTL"],
+ num_period_compare="10",
+ groupby=['country_name'])),
+ Slice(
+ slice_name="% Rural",
+ viz_type='world_map',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type='world_map',
+ metric= "sum__SP_RUR_TOTL_ZS",
+ num_period_compare="10",)),
+ Slice(
+ slice_name="Life Expexctancy VS Rural %",
+ viz_type='bubble',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type='bubble',
+ since= "2011-01-01",
+ until= "2011-01-01",
+ series="region",
+ limit="0",
+ entity="country_name",
+ x="sum__SP_RUR_TOTL_ZS",
+ y="sum__SP_DYN_LE00_IN",
+ size="sum__SP_POP_TOTL",
+ max_bubble_size="50",
+ flt_col_1="country_code",
+ flt_op_1= "not in",
+ flt_eq_1="TCA,MNP,DMA,MHL,MCO,SXM,CYM,TUV,IMY,KNA,ASM,ADO,AMA,PLW",
+ num_period_compare="10",)),
+ Slice(
+ slice_name="Rural Breakdown",
+ viz_type='sunburst',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type='sunburst',
+ groupby=["region", "country_name"],
+ secondary_metric="sum__SP_RUR_TOTL",
+ since= "2011-01-01",
+ until= "2011-01-01",)),
+ Slice(
+ slice_name="World's Pop Growth",
+ viz_type='area',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ since="1960-01-01",
+ until="now",
+ viz_type='area',
+ groupby=["region"],)),
+ ]
+ for slc in slices:
+ merge_slice(slc)
+
+ print("Creating a World's Health Bank dashboard")
+ dash_name = "World's Health Bank Dashboard"
+ dash = db.session.query(Dash).filter_by(dashboard_title=dash_name).first()
+
+ if dash:
+ db.session.delete(dash)
+ js = """\
+[
+ {
+ "size_y": 1,
+ "size_x": 3,
+ "col": 1,
+ "slice_id": "269",
+ "row": 1
+ },
+ {
+ "size_y": 3,
+ "size_x": 3,
+ "col": 1,
+ "slice_id": "270",
+ "row": 2
+ },
+ {
+ "size_y": 7,
+ "size_x": 3,
+ "col": 10,
+ "slice_id": "271",
+ "row": 1
+ },
+ {
+ "size_y": 3,
+ "size_x": 6,
+ "col": 1,
+ "slice_id": "272",
+ "row": 5
+ },
+ {
+ "size_y": 4,
+ "size_x": 6,
+ "col": 4,
+ "slice_id": "273",
+ "row": 1
+ },
+ {
+ "size_y": 4,
+ "size_x": 6,
+ "col": 7,
+ "slice_id": "274",
+ "row": 8
+ },
+ {
+ "size_y": 3,
+ "size_x": 3,
+ "col": 7,
+ "slice_id": "275",
+ "row": 5
+ },
+ {
+ "size_y": 4,
+ "size_x": 6,
+ "col": 1,
+ "slice_id": "276",
+ "row": 8
+ }
+]
+ """
+ l = json.loads(js)
+ for i, pos in enumerate(l):
+ pos['slice_id'] = str(slices[i].id)
+ dash = Dash(
+ dashboard_title=dash_name,
+ position_json=json.dumps(l, indent=4),
+ slug="world_health",
+ )
+ for s in slices:
+ dash.slices.append(s)
+ db.session.commit()
+
+
+def load_css_templates():
+ """Loads 2 css templates to demonstrate the feature"""
+ print('Creating default CSS templates')
+ CSS = models.CssTemplate
+
+ obj = db.session.query(CSS).filter_by(template_name='Flat').first()
+ if not obj:
+ obj = CSS(template_name="Flat")
+ css = textwrap.dedent("""\
+ .gridster li.widget {
+ transition: background-color 0.5s ease;
+ background-color: #FAFAFA;
+ border: 1px solid #CCC;
+ overflow: hidden;
+ box-shadow: none;
+ border-radius: 0px;
+ }
+ .gridster li.widget:hover {
+ border: 1px solid #000;
+ background-color: #EAEAEA;
+ }
+ .navbar {
+ transition: opacity 0.5s ease;
+ opacity: 0.05;
+ }
+ .navbar:hover {
+ opacity: 1;
+ }
+ .chart-header .header{
+ font-weight: normal;
+ font-size: 12px;
+ }
+ /*
+ var bnbColors = [
+ //rausch hackb kazan babu lima beach tirol
+ '#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
+ '#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a',
+ '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e',
+ ];
+ */
+ """)
+ obj.css = css
+ db.session.merge(obj)
+ db.session.commit()
+
+ obj = (
+ db.session.query(CSS).filter_by(template_name='Courier Black').first())
+ if not obj:
+ obj = CSS(template_name="Courier Black")
+ css = textwrap.dedent("""\
+ .gridster li.widget {
+ transition: background-color 0.5s ease;
+ background-color: #EEE;
+ border: 2px solid #444;
+ overflow: hidden;
+ border-radius: 15px;
+ box-shadow: none;
+ }
+ h2 {
+ color: white;
+ font-size: 52px;
+ }
+ .navbar {
+ box-shadow: none;
+ }
+ .gridster li.widget:hover {
+ border: 2px solid #000;
+ background-color: #EAEAEA;
+ }
+ .navbar {
+ transition: opacity 0.5s ease;
+ opacity: 0.05;
+ }
+ .navbar:hover {
+ opacity: 1;
+ }
+ .chart-header .header{
+ font-weight: normal;
+ font-size: 12px;
+ }
+ .nvd3 text {
+ font-size: 12px;
+ font-family: inherit;
+ }
+ body{
+ background: #000;
+ font-family: Courier, Monaco, monospace;;
+ }
+ /*
+ var bnbColors = [
+ //rausch hackb kazan babu lima beach tirol
+ '#ff5a5f', '#7b0051', '#007A87', '#00d1c1', '#8ce071', '#ffb400', '#b4a76c',
+ '#ff8083', '#cc0086', '#00a1b3', '#00ffeb', '#bbedab', '#ffd266', '#cbc29a',
+ '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e',
+ ];
+ */
+ """)
+ obj.css = css
+ db.session.merge(obj)
+ db.session.commit()
+
+
+def load_birth_names():
+ with gzip.open(os.path.join(DATA_FOLDER, 'birth_names.json.gz')) as f:
+ pdf = pd.read_json(f)
+ pdf.ds = pd.to_datetime(pdf.ds, unit='ms')
+ pdf.to_sql(
+ 'birth_names',
+ db.engine,
+ if_exists='replace',
+ chunksize=500,
+ dtype={
+ 'ds': DateTime,
+ 'gender': String(16),
+ 'state': String(10),
+ 'name': String(255),
+ },
+ index=False)
+ l = []
+ print("Done loading table!")
+ print("-" * 80)
+
+ print("Creating table reference")
+ obj = db.session.query(TBL).filter_by(table_name='birth_names').first()
+ if not obj:
+ obj = TBL(table_name = 'birth_names')
+ obj.main_dttm_col = 'ds'
+ obj.database = get_or_create_db(db.session)
+ obj.is_featured = True
+ db.session.merge(obj)
+ db.session.commit()
+ obj.fetch_metadata()
+ tbl = obj
+
+ defaults = {
+ "compare_lag": "10",
+ "compare_suffix": "o10Y",
+ "datasource_id": "1",
+ "datasource_name": "birth_names",
+ "datasource_type": "table",
+ "flt_op_1": "in",
+ "limit": "25",
+ "granularity": "ds",
+ "groupby": [],
+ "metric": 'sum__num',
+ "metrics": ["sum__num"],
+ "row_limit": config.get("ROW_LIMIT"),
+ "since": "100 years ago",
+ "until": "now",
+ "viz_type": "table",
+ "where": "",
+ "markup_type": "markdown",
+ }
+
+ print("Creating some slices")
+ slices = [
+ Slice(
+ slice_name="Girls",
+ viz_type='table',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ groupby=['name'],
+ flt_col_1='gender',
+ flt_eq_1="girl", row_limit=50)),
+ Slice(
+ slice_name="Boys",
+ viz_type='table',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ groupby=['name'],
+ flt_col_1='gender',
+ flt_eq_1="boy",
+ row_limit=50)),
+ Slice(
+ slice_name="Participants",
+ viz_type='big_number',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type="big_number", granularity="ds",
+ compare_lag="5", compare_suffix="over 5Y")),
+ Slice(
+ slice_name="Genders",
+ viz_type='pie',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type="pie", groupby=['gender'])),
+ Slice(
+ slice_name="Genders by State",
+ viz_type='dist_bar',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ flt_eq_1="other", viz_type="dist_bar",
+ metrics=['sum__sum_girls', 'sum__sum_boys'],
+ groupby=['state'], flt_op_1='not in', flt_col_1='state')),
+ Slice(
+ slice_name="Trends",
+ viz_type='line',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type="line", groupby=['name'],
+ granularity='ds', rich_tooltip='y', show_legend='y')),
+ Slice(
+ slice_name="Title",
+ viz_type='markup',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type="markup", markup_type="html",
+ code="""\
+
+
Birth Names Dashboard
+
+ The source dataset came from
+ [here]
+
+
+
+"""
+ )),
+ Slice(
+ slice_name="Name Cloud",
+ viz_type='word_cloud',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type="word_cloud", size_from="10",
+ series='name', size_to="70", rotation="square",
+ limit='100')),
+ Slice(
+ slice_name="Pivot Table",
+ viz_type='pivot_table',
+ datasource_type='table',
+ table=tbl,
+ params=get_slice_json(
+ defaults,
+ viz_type="pivot_table", metrics=['sum__num'],
+ groupby=['name'], columns=['state'])),
+ ]
+ for slc in slices:
+ merge_slice(slc)
+
+ print("Creating a dashboard")
+ dash = db.session.query(Dash).filter_by(dashboard_title="Births").first()
+
+ if dash:
+ db.session.delete(dash)
+ js = """
+[
+ {
+ "size_y": 4,
+ "size_x": 2,
+ "col": 8,
+ "slice_id": "85",
+ "row": 7
+ },
+ {
+ "size_y": 4,
+ "size_x": 2,
+ "col": 10,
+ "slice_id": "86",
+ "row": 7
+ },
+ {
+ "size_y": 2,
+ "size_x": 2,
+ "col": 1,
+ "slice_id": "87",
+ "row": 1
+ },
+ {
+ "size_y": 2,
+ "size_x": 2,
+ "col": 3,
+ "slice_id": "88",
+ "row": 1
+ },
+ {
+ "size_y": 3,
+ "size_x": 7,
+ "col": 5,
+ "slice_id": "89",
+ "row": 4
+ },
+ {
+ "size_y": 4,
+ "size_x": 7,
+ "col": 1,
+ "slice_id": "90",
+ "row": 7
+ },
+ {
+ "size_y": 3,
+ "size_x": 3,
+ "col": 9,
+ "slice_id": "91",
+ "row": 1
+ },
+ {
+ "size_y": 3,
+ "size_x": 4,
+ "col": 5,
+ "slice_id": "92",
+ "row": 1
+ },
+ {
+ "size_y": 4,
+ "size_x": 4,
+ "col": 1,
+ "slice_id": "93",
+ "row": 3
+ }
+]
+ """
+ l = json.loads(js)
+ for i, pos in enumerate(l):
+ pos['slice_id'] = str(slices[i].id)
+ dash = Dash(
+ dashboard_title="Births",
+ position_json=json.dumps(l, indent=4),
+ slug="births",
+ )
+ for s in slices:
+ dash.slices.append(s)
+ db.session.commit()
diff --git a/dashed/data/birth_names.csv.gz b/dashed/data/birth_names.csv.gz
new file mode 100644
index 000000000..9990ab9cc
Binary files /dev/null and b/dashed/data/birth_names.csv.gz differ
diff --git a/dashed/data/birth_names.json.gz b/dashed/data/birth_names.json.gz
new file mode 100644
index 000000000..2652cf724
Binary files /dev/null and b/dashed/data/birth_names.json.gz differ
diff --git a/dashed/data/countries.json.gz b/dashed/data/countries.json.gz
new file mode 100644
index 000000000..6c71c0c43
Binary files /dev/null and b/dashed/data/countries.json.gz differ
diff --git a/dashed/data/countries.md b/dashed/data/countries.md
new file mode 100644
index 000000000..253a68b9f
--- /dev/null
+++ b/dashed/data/countries.md
@@ -0,0 +1,355 @@
+This data was download from the
+[World's Health Organization's website](http://data.worldbank.org/data-catalog/health-nutrition-and-population-statistics)
+
+Here's the script that was used to massage the data:
+
+ DIR = ""
+ df_country = pd.read_csv(DIR + '/HNP_Country.csv')
+ df_country.columns = ['country_code'] + list(df_country.columns[1:])
+ df_country = df_country[['country_code', 'Region']]
+ df_country.columns = ['country_code', 'region']
+
+ df = pd.read_csv(DIR + '/HNP_Data.csv')
+ del df['Unnamed: 60']
+ df.columns = ['country_name', 'country_code'] + list(df.columns[2:])
+ ndf = df.merge(df_country, how='inner')
+
+ dims = ('country_name', 'country_code', 'region')
+ vv = [str(i) for i in range(1960, 2015)]
+ mdf = pd.melt(ndf, id_vars=dims + ('Indicator Code',), value_vars=vv)
+ mdf['year'] = mdf.variable + '-01-01'
+ dims = dims + ('year',)
+
+ pdf = mdf.pivot_table(values='value', columns='Indicator Code', index=dims)
+ pdf = pdf.reset_index()
+ pdf.to_csv(DIR + '/countries.csv')
+ pdf.to_json(DIR + '/countries.json', orient='records')
+
+Here's the description of the metrics available:
+
+Series | Code Indicator Name
+--- | ---
+NY.GNP.PCAP.CD | GNI per capita, Atlas method (current US$)
+SE.ADT.1524.LT.FM.ZS | Literacy rate, youth (ages 15-24), gender parity index (GPI)
+SE.ADT.1524.LT.MA.ZS | Literacy rate, youth male (% of males ages 15-24)
+SE.ADT.1524.LT.ZS | Literacy rate, youth total (% of people ages 15-24)
+SE.ADT.LITR.FE.ZS | Literacy rate, adult female (% of females ages 15 and above)
+SE.ADT.LITR.MA.ZS | Literacy rate, adult male (% of males ages 15 and above)
+SE.ADT.LITR.ZS | Literacy rate, adult total (% of people ages 15 and above)
+SE.ENR.ORPH | Ratio of school attendance of orphans to school attendance of non-orphans ages 10-14
+SE.PRM.CMPT.FE.ZS | Primary completion rate, female (% of relevant age group)
+SE.PRM.CMPT.MA.ZS | Primary completion rate, male (% of relevant age group)
+SE.PRM.CMPT.ZS | Primary completion rate, total (% of relevant age group)
+SE.PRM.ENRR | School enrollment, primary (% gross)
+SE.PRM.ENRR.FE | School enrollment, primary, female (% gross)
+SE.PRM.ENRR.MA | School enrollment, primary, male (% gross)
+SE.PRM.NENR | School enrollment, primary (% net)
+SE.PRM.NENR.FE | School enrollment, primary, female (% net)
+SE.PRM.NENR.MA | School enrollment, primary, male (% net)
+SE.SEC.ENRR | School enrollment, secondary (% gross)
+SE.SEC.ENRR.FE | School enrollment, secondary, female (% gross)
+SE.SEC.ENRR.MA | School enrollment, secondary, male (% gross)
+SE.SEC.NENR | School enrollment, secondary (% net)
+SE.SEC.NENR.FE | School enrollment, secondary, female (% net)
+SE.SEC.NENR.MA | School enrollment, secondary, male (% net)
+SE.TER.ENRR | School enrollment, tertiary (% gross)
+SE.TER.ENRR.FE | School enrollment, tertiary, female (% gross)
+SE.XPD.TOTL.GD.ZS | Government expenditure on education, total (% of GDP)
+SH.ANM.CHLD.ZS | Prevalence of anemia among children (% of children under 5)
+SH.ANM.NPRG.ZS | Prevalence of anemia among non-pregnant women (% of women ages 15-49)
+SH.CON.1524.FE.ZS | Condom use, population ages 15-24, female (% of females ages 15-24)
+SH.CON.1524.MA.ZS | Condom use, population ages 15-24, male (% of males ages 15-24)
+SH.CON.AIDS.FE.ZS | Condom use at last high-risk sex, adult female (% ages 15-49)
+SH.CON.AIDS.MA.ZS | Condom use at last high-risk sex, adult male (% ages 15-49)
+SH.DTH.COMM.ZS | Cause of death, by communicable diseases and maternal, prenatal and nutrition conditions (% of total)
+SH.DTH.IMRT | Number of infant deaths
+SH.DTH.INJR.ZS | Cause of death, by injury (% of total)
+SH.DTH.MORT | Number of under-five deaths
+SH.DTH.NCOM.ZS | Cause of death, by non-communicable diseases (% of total)
+SH.DTH.NMRT | Number of neonatal deaths
+SH.DYN.AIDS | Adults (ages 15+) living with HIV
+SH.DYN.AIDS.DH | AIDS estimated deaths (UNAIDS estimates)
+SH.DYN.AIDS.FE.ZS | Women's share of population ages 15+ living with HIV (%)
+SH.DYN.AIDS.ZS | Prevalence of HIV, total (% of population ages 15-49)
+SH.DYN.MORT | Mortality rate, under-5 (per 1,000 live births)
+SH.DYN.MORT.FE | Mortality rate, under-5, female (per 1,000 live births)
+SH.DYN.MORT.MA | Mortality rate, under-5, male (per 1,000 live births)
+SH.DYN.NMRT | Mortality rate, neonatal (per 1,000 live births)
+SH.FPL.SATI.ZS | Met need for contraception (% of married women ages 15-49)
+SH.H2O.SAFE.RU.ZS | Improved water source, rural (% of rural population with access)
+SH.H2O.SAFE.UR.ZS | Improved water source, urban (% of urban population with access)
+SH.H2O.SAFE.ZS | Improved water source (% of population with access)
+SH.HIV.0014 | Children (0-14) living with HIV
+SH.HIV.1524.FE.ZS | Prevalence of HIV, female (% ages 15-24)
+SH.HIV.1524.KW.FE.ZS | Comprehensive correct knowledge of HIV/AIDS, ages 15-24, female (2 prevent ways and reject 3 misconceptions)
+SH.HIV.1524.KW.MA.ZS | Comprehensive correct knowledge of HIV/AIDS, ages 15-24, male (2 prevent ways and reject 3 misconceptions)
+SH.HIV.1524.MA.ZS | Prevalence of HIV, male (% ages 15-24)
+SH.HIV.ARTC.ZS | Antiretroviral therapy coverage (% of people living with HIV)
+SH.HIV.KNOW.FE.ZS | % of females ages 15-49 having comprehensive correct knowledge about HIV (2 prevent ways and reject 3 misconceptions)
+SH.HIV.KNOW.MA.ZS | % of males ages 15-49 having comprehensive correct knowledge about HIV (2 prevent ways and reject 3 misconceptions)
+SH.HIV.ORPH | Children orphaned by HIV/AIDS
+SH.HIV.TOTL | Adults (ages 15+) and children (0-14 years) living with HIV
+SH.IMM.HEPB | Immunization, HepB3 (% of one-year-old children)
+SH.IMM.HIB3 | Immunization, Hib3 (% of children ages 12-23 months)
+SH.IMM.IBCG | Immunization, BCG (% of one-year-old children)
+SH.IMM.IDPT | Immunization, DPT (% of children ages 12-23 months)
+SH.IMM.MEAS | Immunization, measles (% of children ages 12-23 months)
+SH.IMM.POL3 | Immunization, Pol3 (% of one-year-old children)
+SH.MED.BEDS.ZS | Hospital beds (per 1,000 people)
+SH.MED.CMHW.P3 | Community health workers (per 1,000 people)
+SH.MED.NUMW.P3 | Nurses and midwives (per 1,000 people)
+SH.MED.PHYS.ZS | Physicians (per 1,000 people)
+SH.MLR.NETS.ZS | Use of insecticide-treated bed nets (% of under-5 population)
+SH.MLR.PREG.ZS | Use of any antimalarial drug (% of pregnant women)
+SH.MLR.SPF2.ZS | Use of Intermittent Preventive Treatment of malaria, 2+ doses of SP/Fansidar (% of pregnant women)
+SH.MLR.TRET.ZS | Children with fever receiving antimalarial drugs (% of children under age 5 with fever)
+SH.MMR.DTHS | Number of maternal deaths
+SH.MMR.LEVE | Number of weeks of maternity leave
+SH.MMR.RISK | Lifetime risk of maternal death (1 in: rate varies by country)
+SH.MMR.RISK.ZS | Lifetime risk of maternal death (%)
+SH.MMR.WAGE.ZS | Maternal leave benefits (% of wages paid in covered period)
+SH.PRG.ANEM | Prevalence of anemia among pregnant women (%)
+SH.PRG.ARTC.ZS | Antiretroviral therapy coverage (% of pregnant women living with HIV)
+SH.PRG.SYPH.ZS | Prevalence of syphilis (% of women attending antenatal care)
+SH.PRV.SMOK.FE | Smoking prevalence, females (% of adults)
+SH.PRV.SMOK.MA | Smoking prevalence, males (% of adults)
+SH.STA.ACSN | Improved sanitation facilities (% of population with access)
+SH.STA.ACSN.RU | Improved sanitation facilities, rural (% of rural population with access)
+SH.STA.ACSN.UR | Improved sanitation facilities, urban (% of urban population with access)
+SH.STA.ANV4.ZS | Pregnant women receiving prenatal care of at least four visits (% of pregnant women)
+SH.STA.ANVC.ZS | Pregnant women receiving prenatal care (%)
+SH.STA.ARIC.ZS | ARI treatment (% of children under 5 taken to a health provider)
+SH.STA.BFED.ZS | Exclusive breastfeeding (% of children under 6 months)
+SH.STA.BRTC.ZS | Births attended by skilled health staff (% of total)
+SH.STA.BRTW.ZS | Low-birthweight babies (% of births)
+SH.STA.DIAB.ZS | Diabetes prevalence (% of population ages 20 to 79)
+SH.STA.IYCF.ZS | Infant and young child feeding practices, all 3 IYCF (% children ages 6-23 months)
+SH.STA.MALN.FE.ZS | Prevalence of underweight, weight for age, female (% of children under 5)
+SH.STA.MALN.MA.ZS | Prevalence of underweight, weight for age, male (% of children under 5)
+SH.STA.MALN.ZS | Prevalence of underweight, weight for age (% of children under 5)
+SH.STA.MALR | Malaria cases reported
+SH.STA.MMRT | Maternal mortality ratio (modeled estimate, per 100,000 live births)
+SH.STA.MMRT.NE | Maternal mortality ratio (national estimate, per 100,000 live births)
+SH.STA.ORCF.ZS | Diarrhea treatment (% of children under 5 receiving oral rehydration and continued feeding)
+SH.STA.ORTH | Diarrhea treatment (% of children under 5 who received ORS packet)
+SH.STA.OW15.FE.ZS | Prevalence of overweight, female (% of female adults)
+SH.STA.OW15.MA.ZS | Prevalence of overweight, male (% of male adults)
+SH.STA.OW15.ZS | Prevalence of overweight (% of adults)
+SH.STA.OWGH.FE.ZS | Prevalence of overweight, weight for height, female (% of children under 5)
+SH.STA.OWGH.MA.ZS | Prevalence of overweight, weight for height, male (% of children under 5)
+SH.STA.OWGH.ZS | Prevalence of overweight, weight for height (% of children under 5)
+SH.STA.PNVC.ZS | Postnatal care coverage (% mothers)
+SH.STA.STNT.FE.ZS | Prevalence of stunting, height for age, female (% of children under 5)
+SH.STA.STNT.MA.ZS | Prevalence of stunting, height for age, male (% of children under 5)
+SH.STA.STNT.ZS | Prevalence of stunting, height for age (% of children under 5)
+SH.STA.WAST.FE.ZS | Prevalence of wasting, weight for height, female (% of children under 5)
+SH.STA.WAST.MA.ZS | Prevalence of wasting, weight for height, male (% of children under 5)
+SH.STA.WAST.ZS | Prevalence of wasting, weight for height (% of children under 5)
+SH.SVR.WAST.FE.ZS | Prevalence of severe wasting, weight for height, female (% of children under 5)
+SH.SVR.WAST.MA.ZS | Prevalence of severe wasting, weight for height, male (% of children under 5)
+SH.SVR.WAST.ZS | Prevalence of severe wasting, weight for height (% of children under 5)
+SH.TBS.CURE.ZS | Tuberculosis treatment success rate (% of new cases)
+SH.TBS.DTEC.ZS | Tuberculosis case detection rate (%, all forms)
+SH.TBS.INCD | Incidence of tuberculosis (per 100,000 people)
+SH.TBS.MORT | Tuberculosis death rate (per 100,000 people)
+SH.TBS.PREV | Prevalence of tuberculosis (per 100,000 population)
+SH.VAC.TTNS.ZS | Newborns protected against tetanus (%)
+SH.XPD.EXTR.ZS | External resources for health (% of total expenditure on health)
+SH.XPD.OOPC.TO.ZS | Out-of-pocket health expenditure (% of total expenditure on health)
+SH.XPD.OOPC.ZS | Out-of-pocket health expenditure (% of private expenditure on health)
+SH.XPD.PCAP | Health expenditure per capita (current US$)
+SH.XPD.PCAP.PP.KD | Health expenditure per capita, PPP (constant 2011 international $)
+SH.XPD.PRIV | Health expenditure, private (% of total health expenditure)
+SH.XPD.PRIV.ZS | Health expenditure, private (% of GDP)
+SH.XPD.PUBL | Health expenditure, public (% of total health expenditure)
+SH.XPD.PUBL.GX.ZS | Health expenditure, public (% of government expenditure)
+SH.XPD.PUBL.ZS | Health expenditure, public (% of GDP)
+SH.XPD.TOTL.CD | Health expenditure, total (current US$)
+SH.XPD.TOTL.ZS | Health expenditure, total (% of GDP)
+SI.POV.NAHC | Poverty headcount ratio at national poverty lines (% of population)
+SI.POV.RUHC | Rural poverty headcount ratio at national poverty lines (% of rural population)
+SI.POV.URHC | Urban poverty headcount ratio at national poverty lines (% of urban population)
+SL.EMP.INSV.FE.ZS | Share of women in wage employment in the nonagricultural sector (% of total nonagricultural employment)
+SL.TLF.TOTL.FE.ZS | Labor force, female (% of total labor force)
+SL.TLF.TOTL.IN | Labor force, total
+SL.UEM.TOTL.FE.ZS | Unemployment, female (% of female labor force) (modeled ILO estimate)
+SL.UEM.TOTL.MA.ZS | Unemployment, male (% of male labor force) (modeled ILO estimate)
+SL.UEM.TOTL.ZS | Unemployment, total (% of total labor force) (modeled ILO estimate)
+SM.POP.NETM | Net migration
+SN.ITK.DEFC | Number of people who are undernourished
+SN.ITK.DEFC.ZS | Prevalence of undernourishment (% of population)
+SN.ITK.SALT.ZS | Consumption of iodized salt (% of households)
+SN.ITK.VITA.ZS | Vitamin A supplementation coverage rate (% of children ages 6-59 months)
+SP.ADO.TFRT | Adolescent fertility rate (births per 1,000 women ages 15-19)
+SP.DYN.AMRT.FE | Mortality rate, adult, female (per 1,000 female adults)
+SP.DYN.AMRT.MA | Mortality rate, adult, male (per 1,000 male adults)
+SP.DYN.CBRT.IN | Birth rate, crude (per 1,000 people)
+SP.DYN.CDRT.IN | Death rate, crude (per 1,000 people)
+SP.DYN.CONU.ZS | Contraceptive prevalence (% of women ages 15-49)
+SP.DYN.IMRT.FE.IN | Mortality rate, infant, female (per 1,000 live births)
+SP.DYN.IMRT.IN | Mortality rate, infant (per 1,000 live births)
+SP.DYN.IMRT.MA.IN | Mortality rate, infant, male (per 1,000 live births)
+SP.DYN.LE00.FE.IN | Life expectancy at birth, female (years)
+SP.DYN.LE00.IN | Life expectancy at birth, total (years)
+SP.DYN.LE00.MA.IN | Life expectancy at birth, male (years)
+SP.DYN.SMAM.FE | Mean age at first marriage, female
+SP.DYN.SMAM.MA | Mean age at first marriage, male
+SP.DYN.TFRT.IN | Fertility rate, total (births per woman)
+SP.DYN.TO65.FE.ZS | Survival to age 65, female (% of cohort)
+SP.DYN.TO65.MA.ZS | Survival to age 65, male (% of cohort)
+SP.DYN.WFRT | Wanted fertility rate (births per woman)
+SP.HOU.FEMA.ZS | Female headed households (% of households with a female head)
+SP.MTR.1519.ZS | Teenage mothers (% of women ages 15-19 who have had children or are currently pregnant)
+SP.POP.0004.FE | Population ages 0-4, female
+SP.POP.0004.FE.5Y | Population ages 0-4, female (% of female population)
+SP.POP.0004.MA | Population ages 0-4, male
+SP.POP.0004.MA.5Y | Population ages 0-4, male (% of male population)
+SP.POP.0014.FE.ZS | Population ages 0-14, female (% of total)
+SP.POP.0014.MA.ZS | Population ages 0-14, male (% of total)
+SP.POP.0014.TO | Population ages 0-14, total
+SP.POP.0014.TO.ZS | Population ages 0-14 (% of total)
+SP.POP.0509.FE | Population ages 5-9, female
+SP.POP.0509.FE.5Y | Population ages 5-9, female (% of female population)
+SP.POP.0509.MA | Population ages 5-9, male
+SP.POP.0509.MA.5Y | Population ages 5-9, male (% of male population)
+SP.POP.1014.FE | Population ages 10-14, female
+SP.POP.1014.FE.5Y | Population ages 10-14, female (% of female population)
+SP.POP.1014.MA | Population ages 10-14, male
+SP.POP.1014.MA.5Y | Population ages 10-14, male (% of male population)
+SP.POP.1519.FE | Population ages 15-19, female
+SP.POP.1519.FE.5Y | Population ages 15-19, female (% of female population)
+SP.POP.1519.MA | Population ages 15-19, male
+SP.POP.1519.MA.5Y | Population ages 15-19, male (% of male population)
+SP.POP.1564.FE.ZS | Population ages 15-64, female (% of total)
+SP.POP.1564.MA.ZS | Population ages 15-64, male (% of total)
+SP.POP.1564.TO | Population ages 15-64, total
+SP.POP.1564.TO.ZS | Population ages 15-64 (% of total)
+SP.POP.2024.FE | Population ages 20-24, female
+SP.POP.2024.FE.5Y | Population ages 20-24, female (% of female population)
+SP.POP.2024.MA | Population ages 20-24, male
+SP.POP.2024.MA.5Y | Population ages 20-24, male (% of male population)
+SP.POP.2529.FE | Population ages 25-29, female
+SP.POP.2529.FE.5Y | Population ages 25-29, female (% of female population)
+SP.POP.2529.MA | Population ages 25-29, male
+SP.POP.2529.MA.5Y | Population ages 25-29, male (% of male population)
+SP.POP.3034.FE | Population ages 30-34, female
+SP.POP.3034.FE.5Y | Population ages 30-34, female (% of female population)
+SP.POP.3034.MA | Population ages 30-34, male
+SP.POP.3034.MA.5Y | Population ages 30-34, male (% of male population)
+SP.POP.3539.FE | Population ages 35-39, female
+SP.POP.3539.FE.5Y | Population ages 35-39, female (% of female population)
+SP.POP.3539.MA | Population ages 35-39, male
+SP.POP.3539.MA.5Y | Population ages 35-39, male (% of male population)
+SP.POP.4044.FE | Population ages 40-44, female
+SP.POP.4044.FE.5Y | Population ages 40-44, female (% of female population)
+SP.POP.4044.MA | Population ages 40-44, male
+SP.POP.4044.MA.5Y | Population ages 40-44, male (% of male population)
+SP.POP.4549.FE | Population ages 45-49, female
+SP.POP.4549.FE.5Y | Population ages 45-49, female (% of female population)
+SP.POP.4549.MA | Population ages 45-49, male
+SP.POP.4549.MA.5Y | Population ages 45-49, male (% of male population)
+SP.POP.5054.FE | Population ages 50-54, female
+SP.POP.5054.FE.5Y | Population ages 50-54, female (% of female population)
+SP.POP.5054.MA | Population ages 50-54, male
+SP.POP.5054.MA.5Y | Population ages 50-54, male (% of male population)
+SP.POP.5559.FE | Population ages 55-59, female
+SP.POP.5559.FE.5Y | Population ages 55-59, female (% of female population)
+SP.POP.5559.MA | Population ages 55-59, male
+SP.POP.5559.MA.5Y | Population ages 55-59, male (% of male population)
+SP.POP.6064.FE | Population ages 60-64, female
+SP.POP.6064.FE.5Y | Population ages 60-64, female (% of female population)
+SP.POP.6064.MA | Population ages 60-64, male
+SP.POP.6064.MA.5Y | Population ages 60-64, male (% of male population)
+SP.POP.6569.FE | Population ages 65-69, female
+SP.POP.6569.FE.5Y | Population ages 65-69, female (% of female population)
+SP.POP.6569.MA | Population ages 65-69, male
+SP.POP.6569.MA.5Y | Population ages 65-69, male (% of male population)
+SP.POP.65UP.FE.ZS | Population ages 65 and above, female (% of total)
+SP.POP.65UP.MA.ZS | Population ages 65 and above, male (% of total)
+SP.POP.65UP.TO | Population ages 65 and above, total
+SP.POP.65UP.TO.ZS | Population ages 65 and above (% of total)
+SP.POP.7074.FE | Population ages 70-74, female
+SP.POP.7074.FE.5Y | Population ages 70-74, female (% of female population)
+SP.POP.7074.MA | Population ages 70-74, male
+SP.POP.7074.MA.5Y | Population ages 70-74, male (% of male population)
+SP.POP.7579.FE | Population ages 75-79, female
+SP.POP.7579.FE.5Y | Population ages 75-79, female (% of female population)
+SP.POP.7579.MA | Population ages 75-79, male
+SP.POP.7579.MA.5Y | Population ages 75-79, male (% of male population)
+SP.POP.80UP.FE | Population ages 80 and above, female
+SP.POP.80UP.FE.5Y | Population ages 80 and above, female (% of female population)
+SP.POP.80UP.MA | Population ages 80 and above, male
+SP.POP.80UP.MA.5Y | Population ages 80 and above, male (% of male population)
+SP.POP.AG00.FE.IN | Age population, age 0, female, interpolated
+SP.POP.AG00.MA.IN | Age population, age 0, male, interpolated
+SP.POP.AG01.FE.IN | Age population, age 01, female, interpolated
+SP.POP.AG01.MA.IN | Age population, age 01, male, interpolated
+SP.POP.AG02.FE.IN | Age population, age 02, female, interpolated
+SP.POP.AG02.MA.IN | Age population, age 02, male, interpolated
+SP.POP.AG03.FE.IN | Age population, age 03, female, interpolated
+SP.POP.AG03.MA.IN | Age population, age 03, male, interpolated
+SP.POP.AG04.FE.IN | Age population, age 04, female, interpolated
+SP.POP.AG04.MA.IN | Age population, age 04, male, interpolated
+SP.POP.AG05.FE.IN | Age population, age 05, female, interpolated
+SP.POP.AG05.MA.IN | Age population, age 05, male, interpolated
+SP.POP.AG06.FE.IN | Age population, age 06, female, interpolated
+SP.POP.AG06.MA.IN | Age population, age 06, male, interpolated
+SP.POP.AG07.FE.IN | Age population, age 07, female, interpolated
+SP.POP.AG07.MA.IN | Age population, age 07, male, interpolated
+SP.POP.AG08.FE.IN | Age population, age 08, female, interpolated
+SP.POP.AG08.MA.IN | Age population, age 08, male, interpolated
+SP.POP.AG09.FE.IN | Age population, age 09, female, interpolated
+SP.POP.AG09.MA.IN | Age population, age 09, male, interpolated
+SP.POP.AG10.FE.IN | Age population, age 10, female, interpolated
+SP.POP.AG10.MA.IN | Age population, age 10, male
+SP.POP.AG11.FE.IN | Age population, age 11, female, interpolated
+SP.POP.AG11.MA.IN | Age population, age 11, male
+SP.POP.AG12.FE.IN | Age population, age 12, female, interpolated
+SP.POP.AG12.MA.IN | Age population, age 12, male
+SP.POP.AG13.FE.IN | Age population, age 13, female, interpolated
+SP.POP.AG13.MA.IN | Age population, age 13, male
+SP.POP.AG14.FE.IN | Age population, age 14, female, interpolated
+SP.POP.AG14.MA.IN | Age population, age 14, male
+SP.POP.AG15.FE.IN | Age population, age 15, female, interpolated
+SP.POP.AG15.MA.IN | Age population, age 15, male, interpolated
+SP.POP.AG16.FE.IN | Age population, age 16, female, interpolated
+SP.POP.AG16.MA.IN | Age population, age 16, male, interpolated
+SP.POP.AG17.FE.IN | Age population, age 17, female, interpolated
+SP.POP.AG17.MA.IN | Age population, age 17, male, interpolated
+SP.POP.AG18.FE.IN | Age population, age 18, female, interpolated
+SP.POP.AG18.MA.IN | Age population, age 18, male, interpolated
+SP.POP.AG19.FE.IN | Age population, age 19, female, interpolated
+SP.POP.AG19.MA.IN | Age population, age 19, male, interpolated
+SP.POP.AG20.FE.IN | Age population, age 20, female, interpolated
+SP.POP.AG20.MA.IN | Age population, age 20, male, interpolated
+SP.POP.AG21.FE.IN | Age population, age 21, female, interpolated
+SP.POP.AG21.MA.IN | Age population, age 21, male, interpolated
+SP.POP.AG22.FE.IN | Age population, age 22, female, interpolated
+SP.POP.AG22.MA.IN | Age population, age 22, male, interpolated
+SP.POP.AG23.FE.IN | Age population, age 23, female, interpolated
+SP.POP.AG23.MA.IN | Age population, age 23, male, interpolated
+SP.POP.AG24.FE.IN | Age population, age 24, female, interpolated
+SP.POP.AG24.MA.IN | Age population, age 24, male, interpolated
+SP.POP.AG25.FE.IN | Age population, age 25, female, interpolated
+SP.POP.AG25.MA.IN | Age population, age 25, male, interpolated
+SP.POP.BRTH.MF | Sex ratio at birth (male births per female births)
+SP.POP.DPND | Age dependency ratio (% of working-age population)
+SP.POP.DPND.OL | Age dependency ratio, old (% of working-age population)
+SP.POP.DPND.YG | Age dependency ratio, young (% of working-age population)
+SP.POP.GROW | Population growth (annual %)
+SP.POP.TOTL | Population, total
+SP.POP.TOTL.FE.IN | Population, female
+SP.POP.TOTL.FE.ZS | Population, female (% of total)
+SP.POP.TOTL.MA.IN | Population, male
+SP.POP.TOTL.MA.ZS | Population, male (% of total)
+SP.REG.BRTH.RU.ZS | Completeness of birth registration, rural (%)
+SP.REG.BRTH.UR.ZS | Completeness of birth registration, urban (%)
+SP.REG.BRTH.ZS | Completeness of birth registration (%)
+SP.REG.DTHS.ZS | Completeness of death registration with cause-of-death information (%)
+SP.RUR.TOTL | Rural population
+SP.RUR.TOTL.ZG | Rural population growth (annual %)
+SP.RUR.TOTL.ZS | Rural population (% of total population)
+SP.URB.GROW | Urban population growth (annual %)
+SP.URB.TOTL | Urban population
+SP.URB.TOTL.IN.ZS | Urban population (% of total)
+SP.UWT.TFRT | Unmet need for contraception (% of married women ages 15-49)
diff --git a/dashed/data/countries.py b/dashed/data/countries.py
new file mode 100644
index 000000000..f81ef32df
--- /dev/null
+++ b/dashed/data/countries.py
@@ -0,0 +1,2494 @@
+"""
+This module contains data related to countries and is used for geo mapping
+"""
+
+countries = [
+ {
+ "name": "Angola",
+ "area": 1246700,
+ "cioc": "ANG",
+ "cca2": "AO",
+ "capital": "Luanda",
+ "lat": -12.5,
+ "lng": 18.5,
+ "cca3": "AGO"
+ },
+ {
+ "name": "Algeria",
+ "area": 2381741,
+ "cioc": "ALG",
+ "cca2": "DZ",
+ "capital": "Algiers",
+ "lat": 28,
+ "lng": 3,
+ "cca3": "DZA"
+ },
+ {
+ "name": "Egypt",
+ "area": 1002450,
+ "cioc": "EGY",
+ "cca2": "EG",
+ "capital": "Cairo",
+ "lat": 27,
+ "lng": 30,
+ "cca3": "EGY"
+ },
+ {
+ "name": "Bangladesh",
+ "area": 147570,
+ "cioc": "BAN",
+ "cca2": "BD",
+ "capital": "Dhaka",
+ "lat": 24,
+ "lng": 90,
+ "cca3": "BGD"
+ },
+ {
+ "name": "Niger",
+ "area": 1267000,
+ "cioc": "NIG",
+ "cca2": "NE",
+ "capital": "Niamey",
+ "lat": 16,
+ "lng": 8,
+ "cca3": "NER"
+ },
+ {
+ "name": "Liechtenstein",
+ "area": 160,
+ "cioc": "LIE",
+ "cca2": "LI",
+ "capital": "Vaduz",
+ "lat": 47.26666666,
+ "lng": 9.53333333,
+ "cca3": "LIE"
+ },
+ {
+ "name": "Namibia",
+ "area": 825615,
+ "cioc": "NAM",
+ "cca2": "NA",
+ "capital": "Windhoek",
+ "lat": -22,
+ "lng": 17,
+ "cca3": "NAM"
+ },
+ {
+ "name": "Bulgaria",
+ "area": 110879,
+ "cioc": "BUL",
+ "cca2": "BG",
+ "capital": "Sofia",
+ "lat": 43,
+ "lng": 25,
+ "cca3": "BGR"
+ },
+ {
+ "name": "Bolivia",
+ "area": 1098581,
+ "cioc": "BOL",
+ "cca2": "BO",
+ "capital": "Sucre",
+ "lat": -17,
+ "lng": -65,
+ "cca3": "BOL"
+ },
+ {
+ "name": "Ghana",
+ "area": 238533,
+ "cioc": "GHA",
+ "cca2": "GH",
+ "capital": "Accra",
+ "lat": 8,
+ "lng": -2,
+ "cca3": "GHA"
+ },
+ {
+ "name": "Cocos (Keeling) Islands",
+ "area": 14,
+ "cioc": "",
+ "cca2": "CC",
+ "capital": "West Island",
+ "lat": -12.5,
+ "lng": 96.83333333,
+ "cca3": "CCK"
+ },
+ {
+ "name": "Pakistan",
+ "area": 881912,
+ "cioc": "PAK",
+ "cca2": "PK",
+ "capital": "Islamabad",
+ "lat": 30,
+ "lng": 70,
+ "cca3": "PAK"
+ },
+ {
+ "name": "Cape Verde",
+ "area": 4033,
+ "cioc": "CPV",
+ "cca2": "CV",
+ "capital": "Praia",
+ "lat": 16,
+ "lng": -24,
+ "cca3": "CPV"
+ },
+ {
+ "name": "Jordan",
+ "area": 89342,
+ "cioc": "JOR",
+ "cca2": "JO",
+ "capital": "Amman",
+ "lat": 31,
+ "lng": 36,
+ "cca3": "JOR"
+ },
+ {
+ "name": "Liberia",
+ "area": 111369,
+ "cioc": "LBR",
+ "cca2": "LR",
+ "capital": "Monrovia",
+ "lat": 6.5,
+ "lng": -9.5,
+ "cca3": "LBR"
+ },
+ {
+ "name": "Libya",
+ "area": 1759540,
+ "cioc": "LBA",
+ "cca2": "LY",
+ "capital": "Tripoli",
+ "lat": 25,
+ "lng": 17,
+ "cca3": "LBY"
+ },
+ {
+ "name": "Malaysia",
+ "area": 330803,
+ "cioc": "MAS",
+ "cca2": "MY",
+ "capital": "Kuala Lumpur",
+ "lat": 2.5,
+ "lng": 112.5,
+ "cca3": "MYS"
+ },
+ {
+ "name": "Dominican Republic",
+ "area": 48671,
+ "cioc": "DOM",
+ "cca2": "DO",
+ "capital": "Santo Domingo",
+ "lat": 19,
+ "lng": -70.66666666,
+ "cca3": "DOM"
+ },
+ {
+ "name": "Puerto Rico",
+ "area": 8870,
+ "cioc": "PUR",
+ "cca2": "PR",
+ "capital": "San Juan",
+ "lat": 18.25,
+ "lng": -66.5,
+ "cca3": "PRI"
+ },
+ {
+ "name": "Mayotte",
+ "area": 374,
+ "cioc": "",
+ "cca2": "YT",
+ "capital": "Mamoudzou",
+ "lat": -12.83333333,
+ "lng": 45.16666666,
+ "cca3": "MYT"
+ },
+ {
+ "name": "North Korea",
+ "area": 120538,
+ "cioc": "PRK",
+ "cca2": "KP",
+ "capital": "Pyongyang",
+ "lat": 40,
+ "lng": 127,
+ "cca3": "PRK"
+ },
+ {
+ "name": "Palestine",
+ "area": 6220,
+ "cioc": "PLE",
+ "cca2": "PS",
+ "capital": "Ramallah",
+ "lat": 31.9,
+ "lng": 35.2,
+ "cca3": "PSE"
+ },
+ {
+ "name": "Tanzania",
+ "area": 945087,
+ "cioc": "TAN",
+ "cca2": "TZ",
+ "capital": "Dodoma",
+ "lat": -6,
+ "lng": 35,
+ "cca3": "TZA"
+ },
+ {
+ "name": "Botswana",
+ "area": 582000,
+ "cioc": "BOT",
+ "cca2": "BW",
+ "capital": "Gaborone",
+ "lat": -22,
+ "lng": 24,
+ "cca3": "BWA"
+ },
+ {
+ "name": "Cambodia",
+ "area": 181035,
+ "cioc": "CAM",
+ "cca2": "KH",
+ "capital": "Phnom Penh",
+ "lat": 13,
+ "lng": 105,
+ "cca3": "KHM"
+ },
+ {
+ "name": "Nicaragua",
+ "area": 130373,
+ "cioc": "NCA",
+ "cca2": "NI",
+ "capital": "Managua",
+ "lat": 13,
+ "lng": -85,
+ "cca3": "NIC"
+ },
+ {
+ "name": "Trinidad and Tobago",
+ "area": 5130,
+ "cioc": "TTO",
+ "cca2": "TT",
+ "capital": "Port of Spain",
+ "lat": 11,
+ "lng": -61,
+ "cca3": "TTO"
+ },
+ {
+ "name": "Ethiopia",
+ "area": 1104300,
+ "cioc": "ETH",
+ "cca2": "ET",
+ "capital": "Addis Ababa",
+ "lat": 8,
+ "lng": 38,
+ "cca3": "ETH"
+ },
+ {
+ "name": "Paraguay",
+ "area": 406752,
+ "cioc": "PAR",
+ "cca2": "PY",
+ "capital": "Asuncion",
+ "lat": -23,
+ "lng": -58,
+ "cca3": "PRY"
+ },
+ {
+ "name": "Hong Kong",
+ "area": 1104,
+ "cioc": "HKG",
+ "cca2": "HK",
+ "capital": "City of Victoria",
+ "lat": 22.267,
+ "lng": 114.188,
+ "cca3": "HKG"
+ },
+ {
+ "name": "Saudi Arabia",
+ "area": 2149690,
+ "cioc": "KSA",
+ "cca2": "SA",
+ "capital": "Riyadh",
+ "lat": 25,
+ "lng": 45,
+ "cca3": "SAU"
+ },
+ {
+ "name": "Lebanon",
+ "area": 10452,
+ "cioc": "LIB",
+ "cca2": "LB",
+ "capital": "Beirut",
+ "lat": 33.83333333,
+ "lng": 35.83333333,
+ "cca3": "LBN"
+ },
+ {
+ "name": "Slovenia",
+ "area": 20273,
+ "cioc": "SLO",
+ "cca2": "SI",
+ "capital": "Ljubljana",
+ "lat": 46.11666666,
+ "lng": 14.81666666,
+ "cca3": "SVN"
+ },
+ {
+ "name": "Burkina Faso",
+ "area": 272967,
+ "cioc": "BUR",
+ "cca2": "BF",
+ "capital": "Ouagadougou",
+ "lat": 13,
+ "lng": -2,
+ "cca3": "BFA"
+ },
+ {
+ "name": "Switzerland",
+ "area": 41284,
+ "cioc": "SUI",
+ "cca2": "CH",
+ "capital": "Bern",
+ "lat": 47,
+ "lng": 8,
+ "cca3": "CHE"
+ },
+ {
+ "name": "Mauritania",
+ "area": 1030700,
+ "cioc": "MTN",
+ "cca2": "MR",
+ "capital": "Nouakchott",
+ "lat": 20,
+ "lng": -12,
+ "cca3": "MRT"
+ },
+ {
+ "name": "Croatia",
+ "area": 56594,
+ "cioc": "CRO",
+ "cca2": "HR",
+ "capital": "Zagreb",
+ "lat": 45.16666666,
+ "lng": 15.5,
+ "cca3": "HRV"
+ },
+ {
+ "name": "Chile",
+ "area": 756102,
+ "cioc": "CHI",
+ "cca2": "CL",
+ "capital": "Santiago",
+ "lat": -30,
+ "lng": -71,
+ "cca3": "CHL"
+ },
+ {
+ "name": "China",
+ "area": 9706961,
+ "cioc": "CHN",
+ "cca2": "CN",
+ "capital": "Beijing",
+ "lat": 35,
+ "lng": 105,
+ "cca3": "CHN"
+ },
+ {
+ "name": "Saint Kitts and Nevis",
+ "area": 261,
+ "cioc": "SKN",
+ "cca2": "KN",
+ "capital": "Basseterre",
+ "lat": 17.33333333,
+ "lng": -62.75,
+ "cca3": "KNA"
+ },
+ {
+ "name": "Sierra Leone",
+ "area": 71740,
+ "cioc": "SLE",
+ "cca2": "SL",
+ "capital": "Freetown",
+ "lat": 8.5,
+ "lng": -11.5,
+ "cca3": "SLE"
+ },
+ {
+ "name": "Jamaica",
+ "area": 10991,
+ "cioc": "JAM",
+ "cca2": "JM",
+ "capital": "Kingston",
+ "lat": 18.25,
+ "lng": -77.5,
+ "cca3": "JAM"
+ },
+ {
+ "name": "San Marino",
+ "area": 61,
+ "cioc": "SMR",
+ "cca2": "SM",
+ "capital": "City of San Marino",
+ "lat": 43.76666666,
+ "lng": 12.41666666,
+ "cca3": "SMR"
+ },
+ {
+ "name": "Gibraltar",
+ "area": 6,
+ "cioc": "",
+ "cca2": "GI",
+ "capital": "Gibraltar",
+ "lat": 36.13333333,
+ "lng": -5.35,
+ "cca3": "GIB"
+ },
+ {
+ "name": "Djibouti",
+ "area": 23200,
+ "cioc": "DJI",
+ "cca2": "DJ",
+ "capital": "Djibouti",
+ "lat": 11.5,
+ "lng": 43,
+ "cca3": "DJI"
+ },
+ {
+ "name": "Guinea",
+ "area": 245857,
+ "cioc": "GUI",
+ "cca2": "GN",
+ "capital": "Conakry",
+ "lat": 11,
+ "lng": -10,
+ "cca3": "GIN"
+ },
+ {
+ "name": "Finland",
+ "area": 338424,
+ "cioc": "FIN",
+ "cca2": "FI",
+ "capital": "Helsinki",
+ "lat": 64,
+ "lng": 26,
+ "cca3": "FIN"
+ },
+ {
+ "name": "Uruguay",
+ "area": 181034,
+ "cioc": "URU",
+ "cca2": "UY",
+ "capital": "Montevideo",
+ "lat": -33,
+ "lng": -56,
+ "cca3": "URY"
+ },
+ {
+ "name": "Thailand",
+ "area": 513120,
+ "cioc": "THA",
+ "cca2": "TH",
+ "capital": "Bangkok",
+ "lat": 15,
+ "lng": 100,
+ "cca3": "THA"
+ },
+ {
+ "name": "Sao Tome and Principe",
+ "area": 964,
+ "cioc": "STP",
+ "cca2": "ST",
+ "capital": "Sao Tome",
+ "lat": 1,
+ "lng": 7,
+ "cca3": "STP"
+ },
+ {
+ "name": "Seychelles",
+ "area": 452,
+ "cioc": "SEY",
+ "cca2": "SC",
+ "capital": "Victoria",
+ "lat": -4.58333333,
+ "lng": 55.66666666,
+ "cca3": "SYC"
+ },
+ {
+ "name": "Nepal",
+ "area": 147181,
+ "cioc": "NEP",
+ "cca2": "NP",
+ "capital": "Kathmandu",
+ "lat": 28,
+ "lng": 84,
+ "cca3": "NPL"
+ },
+ {
+ "name": "Christmas Island",
+ "area": 135,
+ "cioc": "",
+ "cca2": "CX",
+ "capital": "Flying Fish Cove",
+ "lat": -10.5,
+ "lng": 105.66666666,
+ "cca3": "CXR"
+ },
+ {
+ "name": "Laos",
+ "area": 236800,
+ "cioc": "LAO",
+ "cca2": "LA",
+ "capital": "Vientiane",
+ "lat": 18,
+ "lng": 105,
+ "cca3": "LAO"
+ },
+ {
+ "name": "Yemen",
+ "area": 527968,
+ "cioc": "YEM",
+ "cca2": "YE",
+ "capital": "Sana'a",
+ "lat": 15,
+ "lng": 48,
+ "cca3": "YEM"
+ },
+ {
+ "name": "Bouvet Island",
+ "area": 49,
+ "cioc": "",
+ "cca2": "BV",
+ "capital": "",
+ "lat": -54.43333333,
+ "lng": 3.4,
+ "cca3": "BVT"
+ },
+ {
+ "name": "South Africa",
+ "area": 1221037,
+ "cioc": "RSA",
+ "cca2": "ZA",
+ "capital": "Pretoria",
+ "lat": -29,
+ "lng": 24,
+ "cca3": "ZAF"
+ },
+ {
+ "name": "Kiribati",
+ "area": 811,
+ "cioc": "KIR",
+ "cca2": "KI",
+ "capital": "South Tarawa",
+ "lat": 1.41666666,
+ "lng": 173,
+ "cca3": "KIR"
+ },
+ {
+ "name": "Philippines",
+ "area": 342353,
+ "cioc": "PHI",
+ "cca2": "PH",
+ "capital": "Manila",
+ "lat": 13,
+ "lng": 122,
+ "cca3": "PHL"
+ },
+ {
+ "name": "Sint Maarten",
+ "area": 34,
+ "cioc": "",
+ "cca2": "SX",
+ "capital": "Philipsburg",
+ "lat": 18.033333,
+ "lng": -63.05,
+ "cca3": "SXM"
+ },
+ {
+ "name": "Romania",
+ "area": 238391,
+ "cioc": "ROU",
+ "cca2": "RO",
+ "capital": "Bucharest",
+ "lat": 46,
+ "lng": 25,
+ "cca3": "ROU"
+ },
+ {
+ "name": "United States Virgin Islands",
+ "area": 347,
+ "cioc": "ISV",
+ "cca2": "VI",
+ "capital": "Charlotte Amalie",
+ "lat": 18.35,
+ "lng": -64.933333,
+ "cca3": "VIR"
+ },
+ {
+ "name": "Syria",
+ "area": 185180,
+ "cioc": "SYR",
+ "cca2": "SY",
+ "capital": "Damascus",
+ "lat": 35,
+ "lng": 38,
+ "cca3": "SYR"
+ },
+ {
+ "name": "Macau",
+ "area": 30,
+ "cioc": "",
+ "cca2": "MO",
+ "capital": "",
+ "lat": 22.16666666,
+ "lng": 113.55,
+ "cca3": "MAC"
+ },
+ {
+ "name": "Saint Martin",
+ "area": 53,
+ "cioc": "",
+ "cca2": "MF",
+ "capital": "Marigot",
+ "lat": 18.08333333,
+ "lng": -63.95,
+ "cca3": "MAF"
+ },
+ {
+ "name": "Malta",
+ "area": 316,
+ "cioc": "MLT",
+ "cca2": "MT",
+ "capital": "Valletta",
+ "lat": 35.83333333,
+ "lng": 14.58333333,
+ "cca3": "MLT"
+ },
+ {
+ "name": "Kazakhstan",
+ "area": 2724900,
+ "cioc": "KAZ",
+ "cca2": "KZ",
+ "capital": "Astana",
+ "lat": 48,
+ "lng": 68,
+ "cca3": "KAZ"
+ },
+ {
+ "name": "Turks and Caicos Islands",
+ "area": 948,
+ "cioc": "",
+ "cca2": "TC",
+ "capital": "Cockburn Town",
+ "lat": 21.75,
+ "lng": -71.58333333,
+ "cca3": "TCA"
+ },
+ {
+ "name": "French Polynesia",
+ "area": 4167,
+ "cioc": "",
+ "cca2": "PF",
+ "capital": "Papeete",
+ "lat": -15,
+ "lng": -140,
+ "cca3": "PYF"
+ },
+ {
+ "name": "Niue",
+ "area": 260,
+ "cioc": "",
+ "cca2": "NU",
+ "capital": "Alofi",
+ "lat": -19.03333333,
+ "lng": -169.86666666,
+ "cca3": "NIU"
+ },
+ {
+ "name": "Dominica",
+ "area": 751,
+ "cioc": "DMA",
+ "cca2": "DM",
+ "capital": "Roseau",
+ "lat": 15.41666666,
+ "lng": -61.33333333,
+ "cca3": "DMA"
+ },
+ {
+ "name": "Benin",
+ "area": 112622,
+ "cioc": "BEN",
+ "cca2": "BJ",
+ "capital": "Porto-Novo",
+ "lat": 9.5,
+ "lng": 2.25,
+ "cca3": "BEN"
+ },
+ {
+ "name": "French Guiana",
+ "area": 83534,
+ "cioc": "",
+ "cca2": "GF",
+ "capital": "Cayenne",
+ "lat": 4,
+ "lng": -53,
+ "cca3": "GUF"
+ },
+ {
+ "name": "Belgium",
+ "area": 30528,
+ "cioc": "BEL",
+ "cca2": "BE",
+ "capital": "Brussels",
+ "lat": 50.83333333,
+ "lng": 4,
+ "cca3": "BEL"
+ },
+ {
+ "name": "Montserrat",
+ "area": 102,
+ "cioc": "",
+ "cca2": "MS",
+ "capital": "Plymouth",
+ "lat": 16.75,
+ "lng": -62.2,
+ "cca3": "MSR"
+ },
+ {
+ "name": "Togo",
+ "area": 56785,
+ "cioc": "TOG",
+ "cca2": "TG",
+ "capital": "Lome",
+ "lat": 8,
+ "lng": 1.16666666,
+ "cca3": "TGO"
+ },
+ {
+ "name": "Germany",
+ "area": 357114,
+ "cioc": "GER",
+ "cca2": "DE",
+ "capital": "Berlin",
+ "lat": 51,
+ "lng": 9,
+ "cca3": "DEU"
+ },
+ {
+ "name": "Guam",
+ "area": 549,
+ "cioc": "GUM",
+ "cca2": "GU",
+ "capital": "Hagatna",
+ "lat": 13.46666666,
+ "lng": 144.78333333,
+ "cca3": "GUM"
+ },
+ {
+ "name": "Sri Lanka",
+ "area": 65610,
+ "cioc": "SRI",
+ "cca2": "LK",
+ "capital": "Colombo",
+ "lat": 7,
+ "lng": 81,
+ "cca3": "LKA"
+ },
+ {
+ "name": "South Sudan",
+ "area": 619745,
+ "cioc": "",
+ "cca2": "SS",
+ "capital": "Juba",
+ "lat": 7,
+ "lng": 30,
+ "cca3": "SSD"
+ },
+ {
+ "name": "Falkland Islands",
+ "area": 12173,
+ "cioc": "",
+ "cca2": "FK",
+ "capital": "Stanley",
+ "lat": -51.75,
+ "lng": -59,
+ "cca3": "FLK"
+ },
+ {
+ "name": "United Kingdom",
+ "area": 242900,
+ "cioc": "GBR",
+ "cca2": "GB",
+ "capital": "London",
+ "lat": 54,
+ "lng": -2,
+ "cca3": "GBR"
+ },
+ {
+ "name": "Guyana",
+ "area": 214969,
+ "cioc": "GUY",
+ "cca2": "GY",
+ "capital": "Georgetown",
+ "lat": 5,
+ "lng": -59,
+ "cca3": "GUY"
+ },
+ {
+ "name": "Costa Rica",
+ "area": 51100,
+ "cioc": "CRC",
+ "cca2": "CR",
+ "capital": "San Jose",
+ "lat": 10,
+ "lng": -84,
+ "cca3": "CRI"
+ },
+ {
+ "name": "Cameroon",
+ "area": 475442,
+ "cioc": "CMR",
+ "cca2": "CM",
+ "capital": "Yaounde",
+ "lat": 6,
+ "lng": 12,
+ "cca3": "CMR"
+ },
+ {
+ "name": "Morocco",
+ "area": 446550,
+ "cioc": "MAR",
+ "cca2": "MA",
+ "capital": "Rabat",
+ "lat": 32,
+ "lng": -5,
+ "cca3": "MAR"
+ },
+ {
+ "name": "Northern Mariana Islands",
+ "area": 464,
+ "cioc": "",
+ "cca2": "MP",
+ "capital": "Saipan",
+ "lat": 15.2,
+ "lng": 145.75,
+ "cca3": "MNP"
+ },
+ {
+ "name": "Lesotho",
+ "area": 30355,
+ "cioc": "LES",
+ "cca2": "LS",
+ "capital": "Maseru",
+ "lat": -29.5,
+ "lng": 28.5,
+ "cca3": "LSO"
+ },
+ {
+ "name": "Hungary",
+ "area": 93028,
+ "cioc": "HUN",
+ "cca2": "HU",
+ "capital": "Budapest",
+ "lat": 47,
+ "lng": 20,
+ "cca3": "HUN"
+ },
+ {
+ "name": "Turkmenistan",
+ "area": 488100,
+ "cioc": "TKM",
+ "cca2": "TM",
+ "capital": "Ashgabat",
+ "lat": 40,
+ "lng": 60,
+ "cca3": "TKM"
+ },
+ {
+ "name": "Suriname",
+ "area": 163820,
+ "cioc": "SUR",
+ "cca2": "SR",
+ "capital": "Paramaribo",
+ "lat": 4,
+ "lng": -56,
+ "cca3": "SUR"
+ },
+ {
+ "name": "Netherlands",
+ "area": 41850,
+ "cioc": "NED",
+ "cca2": "NL",
+ "capital": "Amsterdam",
+ "lat": 52.5,
+ "lng": 5.75,
+ "cca3": "NLD"
+ },
+ {
+ "name": "Bermuda",
+ "area": 54,
+ "cioc": "BER",
+ "cca2": "BM",
+ "capital": "Hamilton",
+ "lat": 32.33333333,
+ "lng": -64.75,
+ "cca3": "BMU"
+ },
+ {
+ "name": "Heard Island and McDonald Islands",
+ "area": 412,
+ "cioc": "",
+ "cca2": "HM",
+ "capital": "",
+ "lat": -53.1,
+ "lng": 72.51666666,
+ "cca3": "HMD"
+ },
+ {
+ "name": "Chad",
+ "area": 1284000,
+ "cioc": "CHA",
+ "cca2": "TD",
+ "capital": "N'Djamena",
+ "lat": 15,
+ "lng": 19,
+ "cca3": "TCD"
+ },
+ {
+ "name": "Georgia",
+ "area": 69700,
+ "cioc": "GEO",
+ "cca2": "GE",
+ "capital": "Tbilisi",
+ "lat": 42,
+ "lng": 43.5,
+ "cca3": "GEO"
+ },
+ {
+ "name": "Montenegro",
+ "area": 13812,
+ "cioc": "MNE",
+ "cca2": "ME",
+ "capital": "Podgorica",
+ "lat": 42.5,
+ "lng": 19.3,
+ "cca3": "MNE"
+ },
+ {
+ "name": "Mongolia",
+ "area": 1564110,
+ "cioc": "MGL",
+ "cca2": "MN",
+ "capital": "Ulan Bator",
+ "lat": 46,
+ "lng": 105,
+ "cca3": "MNG"
+ },
+ {
+ "name": "Marshall Islands",
+ "area": 181,
+ "cioc": "MHL",
+ "cca2": "MH",
+ "capital": "Majuro",
+ "lat": 9,
+ "lng": 168,
+ "cca3": "MHL"
+ },
+ {
+ "name": "Martinique",
+ "area": 1128,
+ "cioc": "",
+ "cca2": "MQ",
+ "capital": "Fort-de-France",
+ "lat": 14.666667,
+ "lng": -61,
+ "cca3": "MTQ"
+ },
+ {
+ "name": "Belize",
+ "area": 22966,
+ "cioc": "BIZ",
+ "cca2": "BZ",
+ "capital": "Belmopan",
+ "lat": 17.25,
+ "lng": -88.75,
+ "cca3": "BLZ"
+ },
+ {
+ "name": "Norfolk Island",
+ "area": 36,
+ "cioc": "",
+ "cca2": "NF",
+ "capital": "Kingston",
+ "lat": -29.03333333,
+ "lng": 167.95,
+ "cca3": "NFK"
+ },
+ {
+ "name": "Myanmar",
+ "area": 676578,
+ "cioc": "MYA",
+ "cca2": "MM",
+ "capital": "Naypyidaw",
+ "lat": 22,
+ "lng": 98,
+ "cca3": "MMR"
+ },
+ {
+ "name": "Afghanistan",
+ "area": 652230,
+ "cioc": "AFG",
+ "cca2": "AF",
+ "capital": "Kabul",
+ "lat": 33,
+ "lng": 65,
+ "cca3": "AFG"
+ },
+ {
+ "name": "Burundi",
+ "area": 27834,
+ "cioc": "BDI",
+ "cca2": "BI",
+ "capital": "Bujumbura",
+ "lat": -3.5,
+ "lng": 30,
+ "cca3": "BDI"
+ },
+ {
+ "name": "British Virgin Islands",
+ "area": 151,
+ "cioc": "IVB",
+ "cca2": "VG",
+ "capital": "Road Town",
+ "lat": 18.431383,
+ "lng": -64.62305,
+ "cca3": "VGB"
+ },
+ {
+ "name": "Belarus",
+ "area": 207600,
+ "cioc": "BLR",
+ "cca2": "BY",
+ "capital": "Minsk",
+ "lat": 53,
+ "lng": 28,
+ "cca3": "BLR"
+ },
+ {
+ "name": "Saint Barthelemy",
+ "area": 21,
+ "cioc": "",
+ "cca2": "BL",
+ "capital": "Gustavia",
+ "lat": 18.5,
+ "lng": -63.41666666,
+ "cca3": "BLM"
+ },
+ {
+ "name": "Grenada",
+ "area": 344,
+ "cioc": "GRN",
+ "cca2": "GD",
+ "capital": "St. George's",
+ "lat": 12.11666666,
+ "lng": -61.66666666,
+ "cca3": "GRD"
+ },
+ {
+ "name": "Tokelau",
+ "area": 12,
+ "cioc": "",
+ "cca2": "TK",
+ "capital": "Fakaofo",
+ "lat": -9,
+ "lng": -172,
+ "cca3": "TKL"
+ },
+ {
+ "name": "Greece",
+ "area": 131990,
+ "cioc": "GRE",
+ "cca2": "GR",
+ "capital": "Athens",
+ "lat": 39,
+ "lng": 22,
+ "cca3": "GRC"
+ },
+ {
+ "name": "Russia",
+ "area": 17098242,
+ "cioc": "RUS",
+ "cca2": "RU",
+ "capital": "Moscow",
+ "lat": 60,
+ "lng": 100,
+ "cca3": "RUS"
+ },
+ {
+ "name": "Greenland",
+ "area": 2166086,
+ "cioc": "",
+ "cca2": "GL",
+ "capital": "Nuuk",
+ "lat": 72,
+ "lng": -40,
+ "cca3": "GRL"
+ },
+ {
+ "name": "Andorra",
+ "area": 468,
+ "cioc": "AND",
+ "cca2": "AD",
+ "capital": "Andorra la Vella",
+ "lat": 42.5,
+ "lng": 1.5,
+ "cca3": "AND"
+ },
+ {
+ "name": "Mozambique",
+ "area": 801590,
+ "cioc": "MOZ",
+ "cca2": "MZ",
+ "capital": "Maputo",
+ "lat": -18.25,
+ "lng": 35,
+ "cca3": "MOZ"
+ },
+ {
+ "name": "Tajikistan",
+ "area": 143100,
+ "cioc": "TJK",
+ "cca2": "TJ",
+ "capital": "Dushanbe",
+ "lat": 39,
+ "lng": 71,
+ "cca3": "TJK"
+ },
+ {
+ "name": "Haiti",
+ "area": 27750,
+ "cioc": "HAI",
+ "cca2": "HT",
+ "capital": "Port-au-Prince",
+ "lat": 19,
+ "lng": -72.41666666,
+ "cca3": "HTI"
+ },
+ {
+ "name": "Mexico",
+ "area": 1964375,
+ "cioc": "MEX",
+ "cca2": "MX",
+ "capital": "Mexico City",
+ "lat": 23,
+ "lng": -102,
+ "cca3": "MEX"
+ },
+ {
+ "name": "Zimbabwe",
+ "area": 390757,
+ "cioc": "ZIM",
+ "cca2": "ZW",
+ "capital": "Harare",
+ "lat": -20,
+ "lng": 30,
+ "cca3": "ZWE"
+ },
+ {
+ "name": "Saint Lucia",
+ "area": 616,
+ "cioc": "LCA",
+ "cca2": "LC",
+ "capital": "Castries",
+ "lat": 13.88333333,
+ "lng": -60.96666666,
+ "cca3": "LCA"
+ },
+ {
+ "name": "India",
+ "area": 3287590,
+ "cioc": "IND",
+ "cca2": "IN",
+ "capital": "New Delhi",
+ "lat": 20,
+ "lng": 77,
+ "cca3": "IND"
+ },
+ {
+ "name": "Latvia",
+ "area": 64559,
+ "cioc": "LAT",
+ "cca2": "LV",
+ "capital": "Riga",
+ "lat": 57,
+ "lng": 25,
+ "cca3": "LVA"
+ },
+ {
+ "name": "Bhutan",
+ "area": 38394,
+ "cioc": "BHU",
+ "cca2": "BT",
+ "capital": "Thimphu",
+ "lat": 27.5,
+ "lng": 90.5,
+ "cca3": "BTN"
+ },
+ {
+ "name": "Saint Vincent and the Grenadines",
+ "area": 389,
+ "cioc": "VIN",
+ "cca2": "VC",
+ "capital": "Kingstown",
+ "lat": 13.25,
+ "lng": -61.2,
+ "cca3": "VCT"
+ },
+ {
+ "name": "Vietnam",
+ "area": 331212,
+ "cioc": "VIE",
+ "cca2": "VN",
+ "capital": "Hanoi",
+ "lat": 16.16666666,
+ "lng": 107.83333333,
+ "cca3": "VNM"
+ },
+ {
+ "name": "Norway",
+ "area": 323802,
+ "cioc": "NOR",
+ "cca2": "NO",
+ "capital": "Oslo",
+ "lat": 62,
+ "lng": 10,
+ "cca3": "NOR"
+ },
+ {
+ "name": "Czech Republic",
+ "area": 78865,
+ "cioc": "CZE",
+ "cca2": "CZ",
+ "capital": "Prague",
+ "lat": 49.75,
+ "lng": 15.5,
+ "cca3": "CZE"
+ },
+ {
+ "name": "French Southern and Antarctic Lands",
+ "area": 7747,
+ "cioc": "",
+ "cca2": "TF",
+ "capital": "Port-aux-Francais",
+ "lat": -49.25,
+ "lng": 69.167,
+ "cca3": "ATF"
+ },
+ {
+ "name": "Antigua and Barbuda",
+ "area": 442,
+ "cioc": "ANT",
+ "cca2": "AG",
+ "capital": "Saint John's",
+ "lat": 17.05,
+ "lng": -61.8,
+ "cca3": "ATG"
+ },
+ {
+ "name": "Fiji",
+ "area": 18272,
+ "cioc": "FIJ",
+ "cca2": "FJ",
+ "capital": "Suva",
+ "lat": -18,
+ "lng": 175,
+ "cca3": "FJI"
+ },
+ {
+ "name": "British Indian Ocean Territory",
+ "area": 60,
+ "cioc": "",
+ "cca2": "IO",
+ "capital": "Diego Garcia",
+ "lat": -6,
+ "lng": 71.5,
+ "cca3": "IOT"
+ },
+ {
+ "name": "Honduras",
+ "area": 112492,
+ "cioc": "HON",
+ "cca2": "HN",
+ "capital": "Tegucigalpa",
+ "lat": 15,
+ "lng": -86.5,
+ "cca3": "HND"
+ },
+ {
+ "name": "Mauritius",
+ "area": 2040,
+ "cioc": "MRI",
+ "cca2": "MU",
+ "capital": "Port Louis",
+ "lat": -20.28333333,
+ "lng": 57.55,
+ "cca3": "MUS"
+ },
+ {
+ "name": "Antarctica",
+ "area": 14000000,
+ "cioc": "",
+ "cca2": "AQ",
+ "capital": "",
+ "lat": -90,
+ "lng": 0,
+ "cca3": "ATA"
+ },
+ {
+ "name": "Luxembourg",
+ "area": 2586,
+ "cioc": "LUX",
+ "cca2": "LU",
+ "capital": "Luxembourg",
+ "lat": 49.75,
+ "lng": 6.16666666,
+ "cca3": "LUX"
+ },
+ {
+ "name": "Israel",
+ "area": 20770,
+ "cioc": "ISR",
+ "cca2": "IL",
+ "capital": "Jerusalem",
+ "lat": 31.47,
+ "lng": 35.13,
+ "cca3": "ISR"
+ },
+ {
+ "name": "Micronesia",
+ "area": 702,
+ "cioc": "FSM",
+ "cca2": "FM",
+ "capital": "Palikir",
+ "lat": 6.91666666,
+ "lng": 158.25,
+ "cca3": "FSM"
+ },
+ {
+ "name": "Peru",
+ "area": 1285216,
+ "cioc": "PER",
+ "cca2": "PE",
+ "capital": "Lima",
+ "lat": -10,
+ "lng": -76,
+ "cca3": "PER"
+ },
+ {
+ "name": "Reunion",
+ "area": 2511,
+ "cioc": "",
+ "cca2": "RE",
+ "capital": "Saint-Denis",
+ "lat": -21.15,
+ "lng": 55.5,
+ "cca3": "REU"
+ },
+ {
+ "name": "Indonesia",
+ "area": 1904569,
+ "cioc": "INA",
+ "cca2": "ID",
+ "capital": "Jakarta",
+ "lat": -5,
+ "lng": 120,
+ "cca3": "IDN"
+ },
+ {
+ "name": "Vanuatu",
+ "area": 12189,
+ "cioc": "VAN",
+ "cca2": "VU",
+ "capital": "Port Vila",
+ "lat": -16,
+ "lng": 167,
+ "cca3": "VUT"
+ },
+ {
+ "name": "Macedonia",
+ "area": 25713,
+ "cioc": "MKD",
+ "cca2": "MK",
+ "capital": "Skopje",
+ "lat": 41.83333333,
+ "lng": 22,
+ "cca3": "MKD"
+ },
+ {
+ "name": "DR Congo",
+ "area": 2344858,
+ "cioc": "COD",
+ "cca2": "CD",
+ "capital": "Kinshasa",
+ "lat": 0,
+ "lng": 25,
+ "cca3": "COD"
+ },
+ {
+ "name": "Republic of the Congo",
+ "area": 342000,
+ "cioc": "CGO",
+ "cca2": "CG",
+ "capital": "Brazzaville",
+ "lat": -1,
+ "lng": 15,
+ "cca3": "COG"
+ },
+ {
+ "name": "Iceland",
+ "area": 103000,
+ "cioc": "ISL",
+ "cca2": "IS",
+ "capital": "Reykjavik",
+ "lat": 65,
+ "lng": -18,
+ "cca3": "ISL"
+ },
+ {
+ "name": "Guadeloupe",
+ "area": 1628,
+ "cioc": "",
+ "cca2": "GP",
+ "capital": "Basse-Terre",
+ "lat": 16.25,
+ "lng": -61.583333,
+ "cca3": "GLP"
+ },
+ {
+ "name": "Cook Islands",
+ "area": 236,
+ "cioc": "COK",
+ "cca2": "CK",
+ "capital": "Avarua",
+ "lat": -21.23333333,
+ "lng": -159.76666666,
+ "cca3": "COK"
+ },
+ {
+ "name": "Comoros",
+ "area": 1862,
+ "cioc": "COM",
+ "cca2": "KM",
+ "capital": "Moroni",
+ "lat": -12.16666666,
+ "lng": 44.25,
+ "cca3": "COM"
+ },
+ {
+ "name": "Colombia",
+ "area": 1141748,
+ "cioc": "COL",
+ "cca2": "CO",
+ "capital": "Bogota",
+ "lat": 4,
+ "lng": -72,
+ "cca3": "COL"
+ },
+ {
+ "name": "Nigeria",
+ "area": 923768,
+ "cioc": "NGR",
+ "cca2": "NG",
+ "capital": "Abuja",
+ "lat": 10,
+ "lng": 8,
+ "cca3": "NGA"
+ },
+ {
+ "name": "Timor-Leste",
+ "area": 14874,
+ "cioc": "TLS",
+ "cca2": "TL",
+ "capital": "Dili",
+ "lat": -8.83333333,
+ "lng": 125.91666666,
+ "cca3": "TLS"
+ },
+ {
+ "name": "Taiwan",
+ "area": 36193,
+ "cioc": "TPE",
+ "cca2": "TW",
+ "capital": "Taipei",
+ "lat": 23.5,
+ "lng": 121,
+ "cca3": "TWN"
+ },
+ {
+ "name": "Portugal",
+ "area": 92090,
+ "cioc": "POR",
+ "cca2": "PT",
+ "capital": "Lisbon",
+ "lat": 39.5,
+ "lng": -8,
+ "cca3": "PRT"
+ },
+ {
+ "name": "Moldova",
+ "area": 33846,
+ "cioc": "MDA",
+ "cca2": "MD",
+ "capital": "Chisinau",
+ "lat": 47,
+ "lng": 29,
+ "cca3": "MDA"
+ },
+ {
+ "name": "Guernsey",
+ "area": 78,
+ "cioc": "",
+ "cca2": "GG",
+ "capital": "St. Peter Port",
+ "lat": 49.46666666,
+ "lng": -2.58333333,
+ "cca3": "GGY"
+ },
+ {
+ "name": "Madagascar",
+ "area": 587041,
+ "cioc": "MAD",
+ "cca2": "MG",
+ "capital": "Antananarivo",
+ "lat": -20,
+ "lng": 47,
+ "cca3": "MDG"
+ },
+ {
+ "name": "Ecuador",
+ "area": 276841,
+ "cioc": "ECU",
+ "cca2": "EC",
+ "capital": "Quito",
+ "lat": -2,
+ "lng": -77.5,
+ "cca3": "ECU"
+ },
+ {
+ "name": "Senegal",
+ "area": 196722,
+ "cioc": "SEN",
+ "cca2": "SN",
+ "capital": "Dakar",
+ "lat": 14,
+ "lng": -14,
+ "cca3": "SEN"
+ },
+ {
+ "name": "New Zealand",
+ "area": 270467,
+ "cioc": "NZL",
+ "cca2": "NZ",
+ "capital": "Wellington",
+ "lat": -41,
+ "lng": 174,
+ "cca3": "NZL"
+ },
+ {
+ "name": "Maldives",
+ "area": 300,
+ "cioc": "MDV",
+ "cca2": "MV",
+ "capital": "Male",
+ "lat": 3.25,
+ "lng": 73,
+ "cca3": "MDV"
+ },
+ {
+ "name": "American Samoa",
+ "area": 199,
+ "cioc": "ASA",
+ "cca2": "AS",
+ "capital": "Pago Pago",
+ "lat": -14.33333333,
+ "lng": -170,
+ "cca3": "ASM"
+ },
+ {
+ "name": "Saint Pierre and Miquelon",
+ "area": 242,
+ "cioc": "",
+ "cca2": "PM",
+ "capital": "Saint-Pierre",
+ "lat": 46.83333333,
+ "lng": -56.33333333,
+ "cca3": "SPM"
+ },
+ {
+ "name": "Curacao",
+ "area": 444,
+ "cioc": "",
+ "cca2": "CW",
+ "capital": "Willemstad",
+ "lat": 12.116667,
+ "lng": -68.933333,
+ "cca3": "CUW"
+ },
+ {
+ "name": "France",
+ "area": 551695,
+ "cioc": "FRA",
+ "cca2": "FR",
+ "capital": "Paris",
+ "lat": 46,
+ "lng": 2,
+ "cca3": "FRA"
+ },
+ {
+ "name": "Lithuania",
+ "area": 65300,
+ "cioc": "LTU",
+ "cca2": "LT",
+ "capital": "Vilnius",
+ "lat": 56,
+ "lng": 24,
+ "cca3": "LTU"
+ },
+ {
+ "name": "Rwanda",
+ "area": 26338,
+ "cioc": "RWA",
+ "cca2": "RW",
+ "capital": "Kigali",
+ "lat": -2,
+ "lng": 30,
+ "cca3": "RWA"
+ },
+ {
+ "name": "Zambia",
+ "area": 752612,
+ "cioc": "ZAM",
+ "cca2": "ZM",
+ "capital": "Lusaka",
+ "lat": -15,
+ "lng": 30,
+ "cca3": "ZMB"
+ },
+ {
+ "name": "Gambia",
+ "area": 10689,
+ "cioc": "GAM",
+ "cca2": "GM",
+ "capital": "Banjul",
+ "lat": 13.46666666,
+ "lng": -16.56666666,
+ "cca3": "GMB"
+ },
+ {
+ "name": "Wallis and Futuna",
+ "area": 142,
+ "cioc": "",
+ "cca2": "WF",
+ "capital": "Mata-Utu",
+ "lat": -13.3,
+ "lng": -176.2,
+ "cca3": "WLF"
+ },
+ {
+ "name": "Jersey",
+ "area": 116,
+ "cioc": "",
+ "cca2": "JE",
+ "capital": "Saint Helier",
+ "lat": 49.25,
+ "lng": -2.16666666,
+ "cca3": "JEY"
+ },
+ {
+ "name": "Faroe Islands",
+ "area": 1393,
+ "cioc": "",
+ "cca2": "FO",
+ "capital": "Torshavn",
+ "lat": 62,
+ "lng": -7,
+ "cca3": "FRO"
+ },
+ {
+ "name": "Guatemala",
+ "area": 108889,
+ "cioc": "GUA",
+ "cca2": "GT",
+ "capital": "Guatemala City",
+ "lat": 15.5,
+ "lng": -90.25,
+ "cca3": "GTM"
+ },
+ {
+ "name": "Denmark",
+ "area": 43094,
+ "cioc": "DEN",
+ "cca2": "DK",
+ "capital": "Copenhagen",
+ "lat": 56,
+ "lng": 10,
+ "cca3": "DNK"
+ },
+ {
+ "name": "Isle of Man",
+ "area": 572,
+ "cioc": "",
+ "cca2": "IM",
+ "capital": "Douglas",
+ "lat": 54.25,
+ "lng": -4.5,
+ "cca3": "IMN"
+ },
+ {
+ "name": "Australia",
+ "area": 7692024,
+ "cioc": "AUS",
+ "cca2": "AU",
+ "capital": "Canberra",
+ "lat": -27,
+ "lng": 133,
+ "cca3": "AUS"
+ },
+ {
+ "name": "Austria",
+ "area": 83871,
+ "cioc": "AUT",
+ "cca2": "AT",
+ "capital": "Vienna",
+ "lat": 47.33333333,
+ "lng": 13.33333333,
+ "cca3": "AUT"
+ },
+ {
+ "name": "Svalbard and Jan Mayen",
+ "area": -1,
+ "cioc": "",
+ "cca2": "SJ",
+ "capital": "Longyearbyen",
+ "lat": 78,
+ "lng": 20,
+ "cca3": "SJM"
+ },
+ {
+ "name": "Venezuela",
+ "area": 916445,
+ "cioc": "VEN",
+ "cca2": "VE",
+ "capital": "Caracas",
+ "lat": 8,
+ "lng": -66,
+ "cca3": "VEN"
+ },
+ {
+ "name": "Kosovo",
+ "area": 10908,
+ "cioc": "KOS",
+ "cca2": "XK",
+ "capital": "Pristina",
+ "lat": 42.666667,
+ "lng": 21.166667,
+ "cca3": "UNK"
+ },
+ {
+ "name": "Palau",
+ "area": 459,
+ "cioc": "PLW",
+ "cca2": "PW",
+ "capital": "Ngerulmud",
+ "lat": 7.5,
+ "lng": 134.5,
+ "cca3": "PLW"
+ },
+ {
+ "name": "Kenya",
+ "area": 580367,
+ "cioc": "KEN",
+ "cca2": "KE",
+ "capital": "Nairobi",
+ "lat": 1,
+ "lng": 38,
+ "cca3": "KEN"
+ },
+ {
+ "name": "Samoa",
+ "area": 2842,
+ "cioc": "SAM",
+ "cca2": "WS",
+ "capital": "Apia",
+ "lat": -13.58333333,
+ "lng": -172.33333333,
+ "cca3": "WSM"
+ },
+ {
+ "name": "Turkey",
+ "area": 783562,
+ "cioc": "TUR",
+ "cca2": "TR",
+ "capital": "Ankara",
+ "lat": 39,
+ "lng": 35,
+ "cca3": "TUR"
+ },
+ {
+ "name": "Albania",
+ "area": 28748,
+ "cioc": "ALB",
+ "cca2": "AL",
+ "capital": "Tirana",
+ "lat": 41,
+ "lng": 20,
+ "cca3": "ALB"
+ },
+ {
+ "name": "Oman",
+ "area": 309500,
+ "cioc": "OMA",
+ "cca2": "OM",
+ "capital": "Muscat",
+ "lat": 21,
+ "lng": 57,
+ "cca3": "OMN"
+ },
+ {
+ "name": "Tuvalu",
+ "area": 26,
+ "cioc": "TUV",
+ "cca2": "TV",
+ "capital": "Funafuti",
+ "lat": -8,
+ "lng": 178,
+ "cca3": "TUV"
+ },
+ {
+ "name": "Aland Islands",
+ "area": 1580,
+ "cioc": "",
+ "cca2": "AX",
+ "capital": "Mariehamn",
+ "lat": 60.116667,
+ "lng": 19.9,
+ "cca3": "ALA"
+ },
+ {
+ "name": "Brunei",
+ "area": 5765,
+ "cioc": "BRU",
+ "cca2": "BN",
+ "capital": "Bandar Seri Begawan",
+ "lat": 4.5,
+ "lng": 114.66666666,
+ "cca3": "BRN"
+ },
+ {
+ "name": "Tunisia",
+ "area": 163610,
+ "cioc": "TUN",
+ "cca2": "TN",
+ "capital": "Tunis",
+ "lat": 34,
+ "lng": 9,
+ "cca3": "TUN"
+ },
+ {
+ "name": "Pitcairn Islands",
+ "area": 47,
+ "cioc": "",
+ "cca2": "PN",
+ "capital": "Adamstown",
+ "lat": -25.06666666,
+ "lng": -130.1,
+ "cca3": "PCN"
+ },
+ {
+ "name": "Barbados",
+ "area": 430,
+ "cioc": "BAR",
+ "cca2": "BB",
+ "capital": "Bridgetown",
+ "lat": 13.16666666,
+ "lng": -59.53333333,
+ "cca3": "BRB"
+ },
+ {
+ "name": "Brazil",
+ "area": 8515767,
+ "cioc": "BRA",
+ "cca2": "BR",
+ "capital": "Brasilia",
+ "lat": -10,
+ "lng": -55,
+ "cca3": "BRA"
+ },
+ {
+ "name": "Ivory Coast",
+ "area": 322463,
+ "cioc": "CIV",
+ "cca2": "CI",
+ "capital": "Yamoussoukro",
+ "lat": 8,
+ "lng": -5,
+ "cca3": "CIV"
+ },
+ {
+ "name": "Serbia",
+ "area": 88361,
+ "cioc": "SRB",
+ "cca2": "RS",
+ "capital": "Belgrade",
+ "lat": 44,
+ "lng": 21,
+ "cca3": "SRB"
+ },
+ {
+ "name": "Equatorial Guinea",
+ "area": 28051,
+ "cioc": "GEQ",
+ "cca2": "GQ",
+ "capital": "Malabo",
+ "lat": 2,
+ "lng": 10,
+ "cca3": "GNQ"
+ },
+ {
+ "name": "United States",
+ "area": 9372610,
+ "cioc": "USA",
+ "cca2": "US",
+ "capital": "Washington D.C.",
+ "lat": 38,
+ "lng": -97,
+ "cca3": "USA"
+ },
+ {
+ "name": "Qatar",
+ "area": 11586,
+ "cioc": "QAT",
+ "cca2": "QA",
+ "capital": "Doha",
+ "lat": 25.5,
+ "lng": 51.25,
+ "cca3": "QAT"
+ },
+ {
+ "name": "Sweden",
+ "area": 450295,
+ "cioc": "SWE",
+ "cca2": "SE",
+ "capital": "Stockholm",
+ "lat": 62,
+ "lng": 15,
+ "cca3": "SWE"
+ },
+ {
+ "name": "Azerbaijan",
+ "area": 86600,
+ "cioc": "AZE",
+ "cca2": "AZ",
+ "capital": "Baku",
+ "lat": 40.5,
+ "lng": 47.5,
+ "cca3": "AZE"
+ },
+ {
+ "name": "Guinea-Bissau",
+ "area": 36125,
+ "cioc": "GBS",
+ "cca2": "GW",
+ "capital": "Bissau",
+ "lat": 12,
+ "lng": -15,
+ "cca3": "GNB"
+ },
+ {
+ "name": "Swaziland",
+ "area": 17364,
+ "cioc": "SWZ",
+ "cca2": "SZ",
+ "capital": "Lobamba",
+ "lat": -26.5,
+ "lng": 31.5,
+ "cca3": "SWZ"
+ },
+ {
+ "name": "Tonga",
+ "area": 747,
+ "cioc": "TGA",
+ "cca2": "TO",
+ "capital": "Nuku'alofa",
+ "lat": -20,
+ "lng": -175,
+ "cca3": "TON"
+ },
+ {
+ "name": "Canada",
+ "area": 9984670,
+ "cioc": "CAN",
+ "cca2": "CA",
+ "capital": "Ottawa",
+ "lat": 60,
+ "lng": -95,
+ "cca3": "CAN"
+ },
+ {
+ "name": "Ukraine",
+ "area": 603500,
+ "cioc": "UKR",
+ "cca2": "UA",
+ "capital": "Kiev",
+ "lat": 49,
+ "lng": 32,
+ "cca3": "UKR"
+ },
+ {
+ "name": "South Korea",
+ "area": 100210,
+ "cioc": "KOR",
+ "cca2": "KR",
+ "capital": "Seoul",
+ "lat": 37,
+ "lng": 127.5,
+ "cca3": "KOR"
+ },
+ {
+ "name": "Anguilla",
+ "area": 91,
+ "cioc": "",
+ "cca2": "AI",
+ "capital": "The Valley",
+ "lat": 18.25,
+ "lng": -63.16666666,
+ "cca3": "AIA"
+ },
+ {
+ "name": "Central African Republic",
+ "area": 622984,
+ "cioc": "CAF",
+ "cca2": "CF",
+ "capital": "Bangui",
+ "lat": 7,
+ "lng": 21,
+ "cca3": "CAF"
+ },
+ {
+ "name": "Slovakia",
+ "area": 49037,
+ "cioc": "SVK",
+ "cca2": "SK",
+ "capital": "Bratislava",
+ "lat": 48.66666666,
+ "lng": 19.5,
+ "cca3": "SVK"
+ },
+ {
+ "name": "Cyprus",
+ "area": 9251,
+ "cioc": "CYP",
+ "cca2": "CY",
+ "capital": "Nicosia",
+ "lat": 35,
+ "lng": 33,
+ "cca3": "CYP"
+ },
+ {
+ "name": "Bosnia and Herzegovina",
+ "area": 51209,
+ "cioc": "BIH",
+ "cca2": "BA",
+ "capital": "Sarajevo",
+ "lat": 44,
+ "lng": 18,
+ "cca3": "BIH"
+ },
+ {
+ "name": "Singapore",
+ "area": 710,
+ "cioc": "SIN",
+ "cca2": "SG",
+ "capital": "Singapore",
+ "lat": 1.36666666,
+ "lng": 103.8,
+ "cca3": "SGP"
+ },
+ {
+ "name": "South Georgia",
+ "area": 3903,
+ "cioc": "",
+ "cca2": "GS",
+ "capital": "King Edward Point",
+ "lat": -54.5,
+ "lng": -37,
+ "cca3": "SGS"
+ },
+ {
+ "name": "Somalia",
+ "area": 637657,
+ "cioc": "SOM",
+ "cca2": "SO",
+ "capital": "Mogadishu",
+ "lat": 10,
+ "lng": 49,
+ "cca3": "SOM"
+ },
+ {
+ "name": "Uzbekistan",
+ "area": 447400,
+ "cioc": "UZB",
+ "cca2": "UZ",
+ "capital": "Tashkent",
+ "lat": 41,
+ "lng": 64,
+ "cca3": "UZB"
+ },
+ {
+ "name": "Eritrea",
+ "area": 117600,
+ "cioc": "ERI",
+ "cca2": "ER",
+ "capital": "Asmara",
+ "lat": 15,
+ "lng": 39,
+ "cca3": "ERI"
+ },
+ {
+ "name": "Poland",
+ "area": 312679,
+ "cioc": "POL",
+ "cca2": "PL",
+ "capital": "Warsaw",
+ "lat": 52,
+ "lng": 20,
+ "cca3": "POL"
+ },
+ {
+ "name": "Kuwait",
+ "area": 17818,
+ "cioc": "KUW",
+ "cca2": "KW",
+ "capital": "Kuwait City",
+ "lat": 29.5,
+ "lng": 45.75,
+ "cca3": "KWT"
+ },
+ {
+ "name": "Gabon",
+ "area": 267668,
+ "cioc": "GAB",
+ "cca2": "GA",
+ "capital": "Libreville",
+ "lat": -1,
+ "lng": 11.75,
+ "cca3": "GAB"
+ },
+ {
+ "name": "Cayman Islands",
+ "area": 264,
+ "cioc": "CAY",
+ "cca2": "KY",
+ "capital": "George Town",
+ "lat": 19.5,
+ "lng": -80.5,
+ "cca3": "CYM"
+ },
+ {
+ "name": "Vatican City",
+ "area": 0.44,
+ "cioc": "",
+ "cca2": "VA",
+ "capital": "Vatican City",
+ "lat": 41.9,
+ "lng": 12.45,
+ "cca3": "VAT"
+ },
+ {
+ "name": "Estonia",
+ "area": 45227,
+ "cioc": "EST",
+ "cca2": "EE",
+ "capital": "Tallinn",
+ "lat": 59,
+ "lng": 26,
+ "cca3": "EST"
+ },
+ {
+ "name": "Malawi",
+ "area": 118484,
+ "cioc": "MAW",
+ "cca2": "MW",
+ "capital": "Lilongwe",
+ "lat": -13.5,
+ "lng": 34,
+ "cca3": "MWI"
+ },
+ {
+ "name": "Spain",
+ "area": 505992,
+ "cioc": "ESP",
+ "cca2": "ES",
+ "capital": "Madrid",
+ "lat": 40,
+ "lng": -4,
+ "cca3": "ESP"
+ },
+ {
+ "name": "Iraq",
+ "area": 438317,
+ "cioc": "IRQ",
+ "cca2": "IQ",
+ "capital": "Baghdad",
+ "lat": 33,
+ "lng": 44,
+ "cca3": "IRQ"
+ },
+ {
+ "name": "El Salvador",
+ "area": 21041,
+ "cioc": "ESA",
+ "cca2": "SV",
+ "capital": "San Salvador",
+ "lat": 13.83333333,
+ "lng": -88.91666666,
+ "cca3": "SLV"
+ },
+ {
+ "name": "Mali",
+ "area": 1240192,
+ "cioc": "MLI",
+ "cca2": "ML",
+ "capital": "Bamako",
+ "lat": 17,
+ "lng": -4,
+ "cca3": "MLI"
+ },
+ {
+ "name": "Ireland",
+ "area": 70273,
+ "cioc": "IRL",
+ "cca2": "IE",
+ "capital": "Dublin",
+ "lat": 53,
+ "lng": -8,
+ "cca3": "IRL"
+ },
+ {
+ "name": "Iran",
+ "area": 1648195,
+ "cioc": "IRI",
+ "cca2": "IR",
+ "capital": "Tehran",
+ "lat": 32,
+ "lng": 53,
+ "cca3": "IRN"
+ },
+ {
+ "name": "Aruba",
+ "area": 180,
+ "cioc": "ARU",
+ "cca2": "AW",
+ "capital": "Oranjestad",
+ "lat": 12.5,
+ "lng": -69.96666666,
+ "cca3": "ABW"
+ },
+ {
+ "name": "Papua New Guinea",
+ "area": 462840,
+ "cioc": "PNG",
+ "cca2": "PG",
+ "capital": "Port Moresby",
+ "lat": -6,
+ "lng": 147,
+ "cca3": "PNG"
+ },
+ {
+ "name": "Panama",
+ "area": 75417,
+ "cioc": "PAN",
+ "cca2": "PA",
+ "capital": "Panama City",
+ "lat": 9,
+ "lng": -80,
+ "cca3": "PAN"
+ },
+ {
+ "name": "Sudan",
+ "area": 1886068,
+ "cioc": "SUD",
+ "cca2": "SD",
+ "capital": "Khartoum",
+ "lat": 15,
+ "lng": 30,
+ "cca3": "SDN"
+ },
+ {
+ "name": "Solomon Islands",
+ "area": 28896,
+ "cioc": "SOL",
+ "cca2": "SB",
+ "capital": "Honiara",
+ "lat": -8,
+ "lng": 159,
+ "cca3": "SLB"
+ },
+ {
+ "name": "Western Sahara",
+ "area": 266000,
+ "cioc": "",
+ "cca2": "EH",
+ "capital": "El Aaiun",
+ "lat": 24.5,
+ "lng": -13,
+ "cca3": "ESH"
+ },
+ {
+ "name": "Monaco",
+ "area": 2.02,
+ "cioc": "MON",
+ "cca2": "MC",
+ "capital": "Monaco",
+ "lat": 43.73333333,
+ "lng": 7.4,
+ "cca3": "MCO"
+ },
+ {
+ "name": "Italy",
+ "area": 301336,
+ "cioc": "ITA",
+ "cca2": "IT",
+ "capital": "Rome",
+ "lat": 42.83333333,
+ "lng": 12.83333333,
+ "cca3": "ITA"
+ },
+ {
+ "name": "Japan",
+ "area": 377930,
+ "cioc": "JPN",
+ "cca2": "JP",
+ "capital": "Tokyo",
+ "lat": 36,
+ "lng": 138,
+ "cca3": "JPN"
+ },
+ {
+ "name": "Kyrgyzstan",
+ "area": 199951,
+ "cioc": "KGZ",
+ "cca2": "KG",
+ "capital": "Bishkek",
+ "lat": 41,
+ "lng": 75,
+ "cca3": "KGZ"
+ },
+ {
+ "name": "Uganda",
+ "area": 241550,
+ "cioc": "UGA",
+ "cca2": "UG",
+ "capital": "Kampala",
+ "lat": 1,
+ "lng": 32,
+ "cca3": "UGA"
+ },
+ {
+ "name": "New Caledonia",
+ "area": 18575,
+ "cioc": "",
+ "cca2": "NC",
+ "capital": "Noumea",
+ "lat": -21.5,
+ "lng": 165.5,
+ "cca3": "NCL"
+ },
+ {
+ "name": "United Arab Emirates",
+ "area": 83600,
+ "cioc": "UAE",
+ "cca2": "AE",
+ "capital": "Abu Dhabi",
+ "lat": 24,
+ "lng": 54,
+ "cca3": "ARE"
+ },
+ {
+ "name": "Argentina",
+ "area": 2780400,
+ "cioc": "ARG",
+ "cca2": "AR",
+ "capital": "Buenos Aires",
+ "lat": -34,
+ "lng": -64,
+ "cca3": "ARG"
+ },
+ {
+ "name": "Bahamas",
+ "area": 13943,
+ "cioc": "BAH",
+ "cca2": "BS",
+ "capital": "Nassau",
+ "lat": 24.25,
+ "lng": -76,
+ "cca3": "BHS"
+ },
+ {
+ "name": "Bahrain",
+ "area": 765,
+ "cioc": "BRN",
+ "cca2": "BH",
+ "capital": "Manama",
+ "lat": 26,
+ "lng": 50.55,
+ "cca3": "BHR"
+ },
+ {
+ "name": "Armenia",
+ "area": 29743,
+ "cioc": "ARM",
+ "cca2": "AM",
+ "capital": "Yerevan",
+ "lat": 40,
+ "lng": 45,
+ "cca3": "ARM"
+ },
+ {
+ "name": "Nauru",
+ "area": 21,
+ "cioc": "NRU",
+ "cca2": "NR",
+ "capital": "Yaren",
+ "lat": -0.53333333,
+ "lng": 166.91666666,
+ "cca3": "NRU"
+ },
+ {
+ "name": "Cuba",
+ "area": 109884,
+ "cioc": "CUB",
+ "cca2": "CU",
+ "capital": "Havana",
+ "lat": 21.5,
+ "lng": -80,
+ "cca3": "CUB"
+ }
+]
+
+all_lookups = {}
+lookups = ['cioc', 'cca2', 'cca3', 'name']
+for lookup in lookups:
+ all_lookups[lookup] = {}
+ for country in countries:
+ all_lookups[lookup][country[lookup].lower()] = country
+
+def get(field, symbol):
+ """
+ Get country data based on a standard code and a symbol
+
+ >>> get('cioc', 'CUB')['name']
+ "Cuba"
+ >>> get('cca2', 'CA')['name']
+ "Canada"
+ """
+ return all_lookups[field].get(symbol.lower())
diff --git a/dashed/forms.py b/dashed/forms.py
new file mode 100644
index 000000000..e5332ce5e
--- /dev/null
+++ b/dashed/forms.py
@@ -0,0 +1,588 @@
+from wtforms import (
+ Form, SelectMultipleField, SelectField, TextField, TextAreaField,
+ BooleanField, IntegerField, HiddenField)
+from wtforms import validators, widgets
+from copy import copy
+from dashed import app
+from collections import OrderedDict
+config = app.config
+
+
+class BetterBooleanField(BooleanField):
+
+ """
+ Fixes behavior of html forms omitting non checked
+ (which doesn't distinguish False from NULL/missing )
+ If value is unchecked, this hidden fills in False value
+ """
+
+ def __call__(self, **kwargs):
+ html = super(BetterBooleanField, self).__call__(**kwargs)
+ html += u' '.format(self.name)
+ return widgets.HTMLString(html)
+
+
+class SelectMultipleSortableField(SelectMultipleField):
+
+ """Works along with select2sortable to preserves the sort order"""
+
+ def iter_choices(self):
+ d = OrderedDict()
+ for value, label in self.choices:
+ selected = self.data is not None and self.coerce(value) in self.data
+ d[value] = (value, label, selected)
+ if self.data:
+ for value in self.data:
+ if value:
+ yield d.pop(value)
+ while d:
+ yield d.pop(d.keys()[0])
+
+
+class FreeFormSelect(widgets.Select):
+
+ """A WTF widget that allows for free form entry"""
+
+ def __call__(self, field, **kwargs):
+ kwargs.setdefault('id', field.id)
+ if self.multiple:
+ kwargs['multiple'] = True
+ html = ['' % widgets.html_params(name=field.name, **kwargs)]
+ found = False
+ for val, label, selected in field.iter_choices():
+ html.append(self.render_option(val, label, selected))
+ if field.data and val == field.data:
+ found = True
+ if not found:
+ html.insert(1, self.render_option(field.data, field.data, True))
+ html.append(' ')
+ return widgets.HTMLString(''.join(html))
+
+
+class FreeFormSelectField(SelectField):
+
+ """ A WTF SelectField that allows for free form input """
+
+ widget = FreeFormSelect()
+ def pre_validate(self, form):
+ return
+
+
+class OmgWtForm(Form):
+
+ """Dashedification of the WTForm Form object"""
+
+ fieldsets = {}
+ css_classes = dict()
+
+ def get_field(self, fieldname):
+ return getattr(self, fieldname)
+
+ def field_css_classes(self, fieldname):
+ if fieldname in self.css_classes:
+ return " ".join(self.css_classes[fieldname])
+ return ""
+
+
+class FormFactory(object):
+ """Used to create the forms in the explore view dynamically"""
+ series_limits = [0, 5, 10, 25, 50, 100, 500]
+ fieltype_class = {
+ SelectField: 'select2',
+ SelectMultipleField: 'select2',
+ FreeFormSelectField: 'select2_freeform',
+ SelectMultipleSortableField: 'select2Sortable',
+ }
+
+ def __init__(self, viz):
+ self.viz = viz
+ from dashed.viz import viz_types
+ viz = self.viz
+ datasource = viz.datasource
+ default_metric = datasource.metrics_combo[0][0]
+ default_groupby = datasource.groupby_column_names[0]
+ group_by_choices = [(s, s) for s in datasource.groupby_column_names]
+ # Pool of all the fields that can be used in Dashed
+ self.field_dict = {
+ 'viz_type': SelectField(
+ 'Viz',
+ default='table',
+ choices=[(k, v.verbose_name) for k, v in viz_types.items()],
+ description="The type of visualization to display"),
+ 'metrics': SelectMultipleSortableField(
+ 'Metrics', choices=datasource.metrics_combo,
+ default=[default_metric],
+ description="One or many metrics to display"),
+ 'metric': SelectField(
+ 'Metric', choices=datasource.metrics_combo,
+ default=default_metric,
+ description="Chose the metric"),
+ 'stacked_style': SelectField(
+ 'Chart Style', choices=self.choicify(
+ ['stack', 'stream', 'expand']),
+ default='stack',
+ description=""),
+ 'linear_color_scheme': SelectField(
+ 'Color Scheme', choices=self.choicify([
+ 'fire', 'blue_white_yellow', 'white_black',
+ 'black_white']),
+ default='fire',
+ description=""),
+ 'normalize_across': SelectField(
+ 'Normalize Across', choices=self.choicify([
+ 'heatmap', 'x', 'y']),
+ default='heatmap',
+ description=(
+ "Color will be rendered based on a ratio "
+ "of the cell against the sum of across this "
+ "criteria")),
+ 'canvas_image_rendering': SelectField(
+ 'Rendering', choices=(
+ ('pixelated', 'pixelated (Sharp)'),
+ ('auto', 'auto (Smooth)'),
+ ),
+ default='pixelated',
+ description=(
+ "image-rendering CSS attribute of the canvas object that "
+ "defines how the browser scales up the image")),
+ 'xscale_interval': SelectField(
+ 'XScale Interval', choices=self.choicify(range(1, 50)),
+ default='1',
+ description=(
+ "Number of step to take between ticks when "
+ "printing the x scale")),
+ 'yscale_interval': SelectField(
+ 'YScale Interval', choices=self.choicify(range(1, 50)),
+ default='1',
+ description=(
+ "Number of step to take between ticks when "
+ "printing the y scale")),
+ 'bar_stacked': BetterBooleanField(
+ 'Stacked Bars',
+ default=False,
+ description=""),
+ 'secondary_metric': SelectField(
+ 'Color Metric', choices=datasource.metrics_combo,
+ default=default_metric,
+ description="A metric to use for color"),
+ 'country_fieldtype': SelectField(
+ 'Country Field Type',
+ default='cca2',
+ choices=(
+ ('name', 'Full name'),
+ ('cioc', 'code International Olympic Committee (cioc)'),
+ ('cca2', 'code ISO 3166-1 alpha-2 (cca2)'),
+ ('cca3', 'code ISO 3166-1 alpha-3 (cca3)'),
+ ),
+ description=(
+ "The country code standard that Dashed should expect "
+ "to find in the [country] column")),
+ 'groupby': SelectMultipleSortableField(
+ 'Group by',
+ choices=self.choicify(datasource.groupby_column_names),
+ description="One or many fields to group by"),
+ 'columns': SelectMultipleSortableField(
+ 'Columns',
+ choices=self.choicify(datasource.groupby_column_names),
+ description="One or many fields to pivot as columns"),
+ 'all_columns': SelectMultipleSortableField(
+ 'Columns',
+ choices=self.choicify(datasource.column_names),
+ description="Columns to display"),
+ 'all_columns_x': SelectField(
+ 'X',
+ choices=self.choicify(datasource.column_names),
+ description="Columns to display"),
+ 'all_columns_y': SelectField(
+ 'Y',
+ choices=self.choicify(datasource.column_names),
+ description="Columns to display"),
+ 'granularity': FreeFormSelectField(
+ 'Time Granularity', default="one day",
+ choices=self.choicify([
+ 'all',
+ '5 seconds',
+ '30 seconds',
+ '1 minute',
+ '5 minutes',
+ '1 hour',
+ '6 hour',
+ '1 day',
+ '7 days',
+ ]),
+ description=(
+ "The time granularity for the visualization. Note that you "
+ "can type and use simple natural language as in '10 seconds', "
+ "'1 day' or '56 weeks'")),
+ 'link_length': FreeFormSelectField(
+ 'Link Length', default="200",
+ choices=self.choicify([
+ '10',
+ '25',
+ '50',
+ '75',
+ '100',
+ '150',
+ '200',
+ '250',
+ ]),
+ description="Link length in the force layout"),
+ 'charge': FreeFormSelectField(
+ 'Charge', default="-500",
+ choices=self.choicify([
+ '-50',
+ '-75',
+ '-100',
+ '-150',
+ '-200',
+ '-250',
+ '-500',
+ '-1000',
+ '-2500',
+ '-5000',
+ ]),
+ description="Charge in the force layout"),
+ 'granularity_sqla': SelectField(
+ 'Time Column',
+ default=datasource.main_dttm_col or datasource.any_dttm_col,
+ choices=self.choicify(datasource.dttm_cols),
+ description=(
+ "The time column for the visualization. Note that you "
+ "can define arbitrary expression that return a DATETIME "
+ "column in the table editor. Also note that the "
+ "filter bellow is applied against this column or "
+ "expression")),
+ 'resample_rule': FreeFormSelectField(
+ 'Resample Rule', default='',
+ choices=self.choicify(('1T', '1H', '1D', '7D', '1M', '1AS')),
+ description=("Pandas resample rule")),
+ 'resample_how': FreeFormSelectField(
+ 'Resample How', default='',
+ choices=self.choicify(('', 'mean', 'sum', 'median')),
+ description=("Pandas resample how")),
+ 'resample_fillmethod': FreeFormSelectField(
+ 'Resample Fill Method', default='',
+ choices=self.choicify(('', 'ffill', 'bfill')),
+ description=("Pandas resample fill method")),
+ 'since': FreeFormSelectField(
+ 'Since', default="7 days ago",
+ choices=self.choicify([
+ '1 hour ago',
+ '12 hours ago',
+ '1 day ago',
+ '7 days ago',
+ '28 days ago',
+ '90 days ago',
+ '1 year ago'
+ ]),
+ description=(
+ "Timestamp from filter. This supports free form typing and "
+ "natural language as in '1 day ago', '28 days' or '3 years'")),
+ 'until': FreeFormSelectField('Until', default="now",
+ choices=self.choicify([
+ 'now',
+ '1 day ago',
+ '7 days ago',
+ '28 days ago',
+ '90 days ago',
+ '1 year ago'])
+ ),
+ 'max_bubble_size': FreeFormSelectField(
+ 'Max Bubble Size', default="25",
+ choices=self.choicify([
+ '5',
+ '10',
+ '15',
+ '25',
+ '50',
+ '75',
+ '100',
+ ])
+ ),
+ 'row_limit':
+ FreeFormSelectField(
+ 'Row limit',
+ default=config.get("ROW_LIMIT"),
+ choices=self.choicify(
+ [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000])),
+ 'limit':
+ FreeFormSelectField(
+ 'Series limit',
+ choices=self.choicify(self.series_limits),
+ default=50,
+ description=(
+ "Limits the number of time series that get displayed")),
+ 'rolling_type': SelectField(
+ 'Rolling',
+ default='None',
+ choices=[(s, s) for s in ['None', 'mean', 'sum', 'std', 'cumsum']],
+ description=(
+ "Defines a rolling window function to apply, works along "
+ "with the [Periods] text box")),
+ 'rolling_periods': IntegerField(
+ 'Periods',
+ validators=[validators.optional()],
+ description=(
+ "Defines the size of the rolling window function, "
+ "relative to the time granularity selected")),
+ 'series': SelectField(
+ 'Series', choices=group_by_choices,
+ default=default_groupby,
+ description=(
+ "Defines the grouping of entities. "
+ "Each serie is shown as a specific color on the chart and "
+ "has a legend toggle")),
+ 'entity': SelectField('Entity', choices=group_by_choices,
+ default=default_groupby,
+ description="This define the element to be plotted on the chart"),
+ 'x': SelectField(
+ 'X Axis', choices=datasource.metrics_combo,
+ default=default_metric,
+ description="Metric assigned to the [X] axis"),
+ 'y': SelectField('Y Axis', choices=datasource.metrics_combo,
+ default=default_metric,
+ description="Metric assigned to the [Y] axis"),
+ 'size': SelectField(
+ 'Bubble Size',
+ default=default_metric,
+ choices=datasource.metrics_combo),
+ 'url': TextField(
+ 'URL', default='www.airbnb.com',),
+ 'where': TextField(
+ 'Custom WHERE clause', default='',
+ description=(
+ "The text in this box gets included in your query's WHERE "
+ "clause, as an AND to other criteria. You can include "
+ "complex expression, parenthesis and anything else "
+ "supported by the backend it is directed towards.")),
+ 'having': TextField('Custom HAVING clause', default='',
+ description=(
+ "The text in this box gets included in your query's HAVING"
+ " clause, as an AND to other criteria. You can include "
+ "complex expression, parenthesis and anything else "
+ "supported by the backend it is directed towards.")),
+ 'compare_lag': TextField('Comparison Period Lag',
+ description=(
+ "Based on granularity, number of time periods to "
+ "compare against")),
+ 'compare_suffix': TextField('Comparison suffix',
+ description="Suffix to apply after the percentage display"),
+ 'x_axis_format': FreeFormSelectField('X axis format',
+ default='smart_date',
+ choices=[
+ ('smart_date', 'Adaptative formating'),
+ ("%m/%d/%Y", '"%m/%d/%Y" | 01/14/2019'),
+ ("%Y-%m-%d", '"%Y-%m-%d" | 2019-01-14'),
+ ("%Y-%m-%d %H:%M:%S",
+ '"%Y-%m-%d %H:%M:%S" | 2019-01-14 01:32:10'),
+ ("%H:%M:%S", '"%H:%M:%S" | 01:32:10'),
+ ],
+ description="D3 format syntax for y axis "
+ "https://github.com/mbostock/\n"
+ "d3/wiki/Formatting"),
+ 'y_axis_format': FreeFormSelectField('Y axis format',
+ default='.3s',
+ choices=[
+ ('.3s', '".3s" | 12.3k'),
+ ('.3%', '".3%" | 1234543.210%'),
+ ('.4r', '".4r" | 12350'),
+ ('.3f', '".3f" | 12345.432'),
+ ('+,', '"+," | +12,345.4321'),
+ ('$,.2f', '"$,.2f" | $12,345.43'),
+ ],
+ description="D3 format syntax for y axis "
+ "https://github.com/mbostock/\n"
+ "d3/wiki/Formatting"),
+ 'markup_type': SelectField(
+ "Markup Type",
+ choices=self.choicify(['markdown', 'html']),
+ default="markdown",
+ description="Pick your favorite markup language"),
+ 'rotation': SelectField(
+ "Rotation",
+ choices=[(s, s) for s in ['random', 'flat', 'square']],
+ default="random",
+ description="Rotation to apply to words in the cloud"),
+ 'line_interpolation': SelectField(
+ "Line Style",
+ choices=self.choicify([
+ 'linear', 'basis', 'cardinal', 'monotone',
+ 'step-before', 'step-after']),
+ default='linear',
+ description="Line interpolation as defined by d3.js"),
+ 'code': TextAreaField(
+ "Code", description="Put your code here", default=''),
+ 'pandas_aggfunc': SelectField(
+ "Aggregation function",
+ choices=self.choicify([
+ 'sum', 'mean', 'min', 'max', 'median', 'stdev', 'var']),
+ default='sum',
+ description=(
+ "Aggregate function to apply when pivoting and "
+ "computing the total rows and columns")),
+ 'size_from': TextField(
+ "Font Size From",
+ default="20",
+ description="Font size for the smallest value in the list"),
+ 'size_to': TextField(
+ "Font Size To",
+ default="150",
+ description="Font size for the biggest value in the list"),
+ 'show_brush': BetterBooleanField(
+ "Range Filter", default=False,
+ description=(
+ "Whether to display the time range interactive selector")),
+ 'show_datatable': BetterBooleanField(
+ "Data Table", default=False,
+ description="Whether to display the interactive data table"),
+ 'include_search': BetterBooleanField(
+ "Search Box", default=False,
+ description=(
+ "Whether to include a client side search box")),
+ 'show_bubbles': BetterBooleanField(
+ "Show Bubbles", default=False,
+ description=(
+ "Whether to display bubbles on top of countries")),
+ 'show_legend': BetterBooleanField(
+ "Legend", default=True,
+ description="Whether to display the legend (toggles)"),
+ 'x_axis_showminmax': BetterBooleanField(
+ "X bounds", default=True,
+ description=(
+ "Whether to display the min and max values of the X axis")),
+ 'rich_tooltip': BetterBooleanField(
+ "Rich Tooltip", default=True,
+ description=(
+ "The rich tooltip shows a list of all series for that"
+ " point in time")),
+ 'y_axis_zero': BetterBooleanField(
+ "Y Axis Zero", default=False,
+ description=(
+ "Force the Y axis to start at 0 instead of the minimum "
+ "value")),
+ 'y_log_scale': BetterBooleanField(
+ "Y Log", default=False,
+ description="Use a log scale for the Y axis"),
+ 'x_log_scale': BetterBooleanField(
+ "X Log", default=False,
+ description="Use a log scale for the X axis"),
+ 'donut': BetterBooleanField(
+ "Donut", default=False,
+ description="Do you want a donut or a pie?"),
+ 'contribution': BetterBooleanField(
+ "Contribution", default=False,
+ description="Compute the contribution to the total"),
+ 'num_period_compare': IntegerField(
+ "Period Ratio", default=None,
+ validators=[validators.optional()],
+ description=(
+ "[integer] Number of period to compare against, "
+ "this is relative to the granularity selected")),
+ 'time_compare': TextField(
+ "Time Shift",
+ default="",
+ description=(
+ "Overlay a timeseries from a "
+ "relative time period. Expects relative time delta "
+ "in natural language (example: 24 hours, 7 days, "
+ "56 weeks, 365 days")),
+ }
+
+ @staticmethod
+ def choicify(l):
+ return [("{}".format(obj), "{}".format(obj)) for obj in l]
+
+ def get_form(self):
+ """Returns a form object based on the viz/datasource/context"""
+ viz = self.viz
+ field_css_classes = {}
+ for name, obj in self.field_dict.items():
+ field_css_classes[name] = ['form-control']
+ s = self.fieltype_class.get(obj.field_class)
+ if s:
+ field_css_classes[name] += [s]
+
+ for field in ('show_brush', 'show_legend', 'rich_tooltip'):
+ field_css_classes[field] += ['input-sm']
+
+ class QueryForm(OmgWtForm):
+ fieldsets = copy(viz.fieldsets)
+ css_classes = field_css_classes
+ standalone = HiddenField()
+ async = HiddenField()
+ extra_filters = HiddenField()
+ json = HiddenField()
+ slice_id = HiddenField()
+ slice_name = HiddenField()
+ previous_viz_type = HiddenField(default=viz.viz_type)
+ collapsed_fieldsets = HiddenField()
+ viz_type = self.field_dict.get('viz_type')
+
+ filter_cols = viz.datasource.filterable_column_names or ['']
+ for i in range(10):
+ setattr(QueryForm, 'flt_col_' + str(i), SelectField(
+ 'Filter 1',
+ default=filter_cols[0],
+ choices=self.choicify(filter_cols)))
+ setattr(QueryForm, 'flt_op_' + str(i), SelectField(
+ 'Filter 1',
+ default='in',
+ choices=self.choicify(['in', 'not in'])))
+ setattr(
+ QueryForm, 'flt_eq_' + str(i),
+ TextField("Super", default=''))
+
+ for field in viz.flat_form_fields():
+ setattr(QueryForm, field, self.field_dict[field])
+
+ def add_to_form(attrs):
+ for attr in attrs:
+ setattr(QueryForm, attr, self.field_dict[attr])
+
+ # datasource type specific form elements
+ if viz.datasource.__class__.__name__ == 'SqlaTable':
+ QueryForm.fieldsets += ({
+ 'label': 'SQL',
+ 'fields': ['where', 'having'],
+ 'description': (
+ "This section exposes ways to include snippets of "
+ "SQL in your query"),
+ },)
+ add_to_form(('where', 'having'))
+ grains = viz.datasource.database.grains()
+
+ if not viz.datasource.any_dttm_col:
+ return QueryForm
+ if grains:
+ time_fields = ('granularity_sqla', 'time_grain_sqla')
+ self.field_dict['time_grain_sqla'] = SelectField(
+ 'Time Grain',
+ choices=self.choicify((grain.name for grain in grains)),
+ default="Time Column",
+ description=(
+ "The time granularity for the visualization. This "
+ "applies a date transformation to alter "
+ "your time column and defines a new time granularity."
+ "The options here are defined on a per database "
+ "engine basis in the Dashed source code"))
+ add_to_form(time_fields)
+ field_css_classes['time_grain_sqla'] = ['form-control', 'select2']
+ field_css_classes['granularity_sqla'] = ['form-control', 'select2']
+ else:
+ time_fields = 'granularity_sqla'
+ add_to_form((time_fields, ))
+ else:
+ time_fields = 'granularity'
+ add_to_form(('granularity',))
+ field_css_classes['granularity'] = ['form-control', 'select2']
+ add_to_form(('since', 'until'))
+
+ QueryForm.fieldsets = ({
+ 'label': 'Time',
+ 'fields': (
+ time_fields,
+ ('since', 'until'),
+ ),
+ 'description': "Time related form attributes",
+ },) + tuple(QueryForm.fieldsets)
+ return QueryForm
diff --git a/dashed/migrations/README b/dashed/migrations/README
new file mode 100755
index 000000000..98e4f9c44
--- /dev/null
+++ b/dashed/migrations/README
@@ -0,0 +1 @@
+Generic single-database configuration.
\ No newline at end of file
diff --git a/dashed/migrations/__init__.py b/dashed/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/dashed/migrations/alembic.ini b/dashed/migrations/alembic.ini
new file mode 100644
index 000000000..f8ed4801f
--- /dev/null
+++ b/dashed/migrations/alembic.ini
@@ -0,0 +1,45 @@
+# A generic, single database configuration.
+
+[alembic]
+# template used to generate migration files
+# file_template = %%(rev)s_%%(slug)s
+
+# set to 'true' to run the environment during
+# the 'revision' command, regardless of autogenerate
+# revision_environment = false
+
+
+# Logging configuration
+[loggers]
+keys = root,sqlalchemy,alembic
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+qualname =
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+
+[logger_alembic]
+level = INFO
+handlers =
+qualname = alembic
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(levelname)-5.5s [%(name)s] %(message)s
+datefmt = %H:%M:%S
diff --git a/dashed/migrations/env.py b/dashed/migrations/env.py
new file mode 100755
index 000000000..e3713a3e4
--- /dev/null
+++ b/dashed/migrations/env.py
@@ -0,0 +1,88 @@
+from __future__ import with_statement
+from alembic import context
+from sqlalchemy import engine_from_config, pool
+from logging.config import fileConfig
+import logging
+from flask.ext.appbuilder import Base
+
+# this is the Alembic Config object, which provides
+# access to the values within the .ini file in use.
+config = context.config
+
+# Interpret the config file for Python logging.
+# This line sets up loggers basically.
+fileConfig(config.config_file_name)
+logger = logging.getLogger('alembic.env')
+
+# add your model's MetaData object here
+# for 'autogenerate' support
+# from myapp import mymodel
+from flask import current_app
+config.set_main_option('sqlalchemy.url',
+ current_app.config.get('SQLALCHEMY_DATABASE_URI'))
+target_metadata = Base.metadata
+
+# other values from the config, defined by the needs of env.py,
+# can be acquired:
+# my_important_option = config.get_main_option("my_important_option")
+# ... etc.
+
+
+def run_migrations_offline():
+ """Run migrations in 'offline' mode.
+
+ This configures the context with just a URL
+ and not an Engine, though an Engine is acceptable
+ here as well. By skipping the Engine creation
+ we don't even need a DBAPI to be available.
+
+ Calls to context.execute() here emit the given string to the
+ script output.
+
+ """
+ url = config.get_main_option("sqlalchemy.url")
+ context.configure(url=url)
+
+ with context.begin_transaction():
+ context.run_migrations()
+
+
+def run_migrations_online():
+ """Run migrations in 'online' mode.
+
+ In this scenario we need to create an Engine
+ and associate a connection with the context.
+
+ """
+
+ # this callback is used to prevent an auto-migration from being generated
+ # when there are no changes to the schema
+ # reference: http://alembic.readthedocs.org/en/latest/cookbook.html
+ def process_revision_directives(context, revision, directives):
+ if getattr(config.cmd_opts, 'autogenerate', False):
+ script = directives[0]
+ if script.upgrade_ops.is_empty():
+ directives[:] = []
+ logger.info('No changes in schema detected.')
+
+ engine = engine_from_config(config.get_section(config.config_ini_section),
+ prefix='sqlalchemy.',
+ poolclass=pool.NullPool)
+
+ connection = engine.connect()
+ context.configure(connection=connection,
+ target_metadata=target_metadata,
+ #compare_type=True,
+ process_revision_directives=process_revision_directives,
+ **current_app.extensions['migrate'].configure_args)
+
+ try:
+ with context.begin_transaction():
+ context.run_migrations()
+ finally:
+ connection.close()
+
+if context.is_offline_mode():
+ run_migrations_offline()
+else:
+ run_migrations_online()
diff --git a/dashed/migrations/script.py.mako b/dashed/migrations/script.py.mako
new file mode 100755
index 000000000..95702017e
--- /dev/null
+++ b/dashed/migrations/script.py.mako
@@ -0,0 +1,22 @@
+"""${message}
+
+Revision ID: ${up_revision}
+Revises: ${down_revision}
+Create Date: ${create_date}
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = ${repr(up_revision)}
+down_revision = ${repr(down_revision)}
+
+from alembic import op
+import sqlalchemy as sa
+${imports if imports else ""}
+
+def upgrade():
+ ${upgrades if upgrades else "pass"}
+
+
+def downgrade():
+ ${downgrades if downgrades else "pass"}
diff --git a/dashed/migrations/versions/12d55656cbca_is_featured.py b/dashed/migrations/versions/12d55656cbca_is_featured.py
new file mode 100644
index 000000000..315822374
--- /dev/null
+++ b/dashed/migrations/versions/12d55656cbca_is_featured.py
@@ -0,0 +1,23 @@
+"""is_featured
+
+Revision ID: 12d55656cbca
+Revises: 55179c7f25c7
+Create Date: 2015-12-14 13:37:17.374852
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '12d55656cbca'
+down_revision = '55179c7f25c7'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('tables', sa.Column('is_featured', sa.Boolean(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('tables', 'is_featured')
+
diff --git a/dashed/migrations/versions/18e88e1cc004_making_audit_nullable.py b/dashed/migrations/versions/18e88e1cc004_making_audit_nullable.py
new file mode 100644
index 000000000..0143aad58
--- /dev/null
+++ b/dashed/migrations/versions/18e88e1cc004_making_audit_nullable.py
@@ -0,0 +1,99 @@
+"""making audit nullable
+
+Revision ID: 18e88e1cc004
+Revises: 430039611635
+Create Date: 2016-03-13 21:30:24.833107
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '18e88e1cc004'
+down_revision = '430039611635'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ try:
+ op.alter_column(
+ 'clusters', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column(
+ 'clusters', 'created_on',
+ existing_type=sa.DATETIME(), nullable=True)
+ op.drop_constraint(None, 'columns', type_='foreignkey')
+ op.drop_constraint(None, 'columns', type_='foreignkey')
+ op.drop_column('columns', 'created_on')
+ op.drop_column('columns', 'created_by_fk')
+ op.drop_column('columns', 'changed_on')
+ op.drop_column('columns', 'changed_by_fk')
+ op.alter_column('css_templates', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('css_templates', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('dashboards', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('dashboards', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.create_unique_constraint(None, 'dashboards', ['slug'])
+ op.alter_column('datasources', 'changed_by_fk',
+ existing_type=sa.INTEGER(),
+ nullable=True)
+ op.alter_column('datasources', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('datasources', 'created_by_fk',
+ existing_type=sa.INTEGER(),
+ nullable=True)
+ op.alter_column('datasources', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('dbs', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('dbs', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('slices', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('slices', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('sql_metrics', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('sql_metrics', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('table_columns', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('table_columns', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('tables', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('tables', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('url', 'changed_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ op.alter_column('url', 'created_on',
+ existing_type=sa.DATETIME(),
+ nullable=True)
+ ### end Alembic commands ###
+ except:
+ pass
+
+
+def downgrade():
+ pass
diff --git a/dashed/migrations/versions/1a48a5411020_adding_slug_to_dash.py b/dashed/migrations/versions/1a48a5411020_adding_slug_to_dash.py
new file mode 100644
index 000000000..c6b88642b
--- /dev/null
+++ b/dashed/migrations/versions/1a48a5411020_adding_slug_to_dash.py
@@ -0,0 +1,26 @@
+"""adding slug to dash
+
+Revision ID: 1a48a5411020
+Revises: 289ce07647b
+Create Date: 2015-12-04 09:42:16.973264
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '1a48a5411020'
+down_revision = '289ce07647b'
+
+from alembic import op
+import sqlalchemy as sa
+
+def upgrade():
+ op.add_column('dashboards', sa.Column('slug', sa.String(length=255), nullable=True))
+ try:
+ op.create_unique_constraint('idx_unique_slug', 'dashboards', ['slug'])
+ except:
+ pass
+
+
+def downgrade():
+ op.drop_constraint(None, 'dashboards', type_='unique')
+ op.drop_column('dashboards', 'slug')
diff --git a/dashed/migrations/versions/1e2841a4128_.py b/dashed/migrations/versions/1e2841a4128_.py
new file mode 100644
index 000000000..330b3b217
--- /dev/null
+++ b/dashed/migrations/versions/1e2841a4128_.py
@@ -0,0 +1,21 @@
+"""empty message
+
+Revision ID: 1e2841a4128
+Revises: 5a7bad26f2a7
+Create Date: 2015-10-05 22:11:00.537054
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '1e2841a4128'
+down_revision = '5a7bad26f2a7'
+
+from alembic import op
+import sqlalchemy as sa
+
+def upgrade():
+ op.add_column('table_columns', sa.Column('expression', sa.Text(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('table_columns', 'expression')
diff --git a/dashed/migrations/versions/2591d77e9831_user_id.py b/dashed/migrations/versions/2591d77e9831_user_id.py
new file mode 100644
index 000000000..4fac61ce9
--- /dev/null
+++ b/dashed/migrations/versions/2591d77e9831_user_id.py
@@ -0,0 +1,26 @@
+"""user_id
+
+Revision ID: 2591d77e9831
+Revises: 12d55656cbca
+Create Date: 2015-12-15 17:02:45.128709
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2591d77e9831'
+down_revision = '12d55656cbca'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ with op.batch_alter_table('tables') as batch_op:
+ batch_op.add_column(sa.Column('user_id', sa.Integer()))
+ batch_op.create_foreign_key('user_id', 'ab_user', ['user_id'], ['id'])
+
+
+def downgrade():
+ with op.batch_alter_table('tables') as batch_op:
+ batch_op.drop_constraint('user_id', type_='foreignkey')
+ batch_op.drop_column('user_id')
diff --git a/dashed/migrations/versions/289ce07647b_add_encrypted_password_field.py b/dashed/migrations/versions/289ce07647b_add_encrypted_password_field.py
new file mode 100644
index 000000000..6d64887b2
--- /dev/null
+++ b/dashed/migrations/versions/289ce07647b_add_encrypted_password_field.py
@@ -0,0 +1,28 @@
+"""Add encrypted password field
+
+Revision ID: 289ce07647b
+Revises: 2929af7925ed
+Create Date: 2015-11-21 11:18:00.650587
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '289ce07647b'
+down_revision = '2929af7925ed'
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy_utils.types.encrypted import EncryptedType
+
+
+def upgrade():
+ op.add_column(
+ 'dbs',
+ sa.Column(
+ 'password',
+ EncryptedType(sa.String(1024)),
+ nullable=True))
+
+
+def downgrade():
+ op.drop_column('dbs', 'password')
diff --git a/dashed/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py b/dashed/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py
new file mode 100644
index 000000000..85b54bc5c
--- /dev/null
+++ b/dashed/migrations/versions/2929af7925ed_tz_offsets_in_data_sources.py
@@ -0,0 +1,23 @@
+"""TZ offsets in data sources
+
+Revision ID: 2929af7925ed
+Revises: 1e2841a4128
+Create Date: 2015-10-19 20:54:00.565633
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '2929af7925ed'
+down_revision = '1e2841a4128'
+
+from alembic import op
+import sqlalchemy as sa
+
+def upgrade():
+ op.add_column('datasources', sa.Column('offset', sa.Integer(), nullable=True))
+ op.add_column('tables', sa.Column('offset', sa.Integer(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('tables', 'offset')
+ op.drop_column('datasources', 'offset')
diff --git a/dashed/migrations/versions/315b3f4da9b0_adding_log_model.py b/dashed/migrations/versions/315b3f4da9b0_adding_log_model.py
new file mode 100644
index 000000000..d9fdfacce
--- /dev/null
+++ b/dashed/migrations/versions/315b3f4da9b0_adding_log_model.py
@@ -0,0 +1,30 @@
+"""adding log model
+
+Revision ID: 315b3f4da9b0
+Revises: 1a48a5411020
+Create Date: 2015-12-04 11:16:58.226984
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '315b3f4da9b0'
+down_revision = '1a48a5411020'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.create_table('logs',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('action', sa.String(length=512), nullable=True),
+ sa.Column('user_id', sa.Integer(), nullable=True),
+ sa.Column('json', sa.Text(), nullable=True),
+ sa.Column('dttm', sa.DateTime(), nullable=True),
+ sa.ForeignKeyConstraint(['user_id'], ['ab_user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+
+
+def downgrade():
+ op.drop_table('logs')
diff --git a/dashed/migrations/versions/430039611635_log_more.py b/dashed/migrations/versions/430039611635_log_more.py
new file mode 100644
index 000000000..aec2b32ed
--- /dev/null
+++ b/dashed/migrations/versions/430039611635_log_more.py
@@ -0,0 +1,23 @@
+"""log more
+
+Revision ID: 430039611635
+Revises: d827694c7555
+Create Date: 2016-02-10 08:47:28.950891
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '430039611635'
+down_revision = 'd827694c7555'
+
+from alembic import op
+import sqlalchemy as sa
+
+def upgrade():
+ op.add_column('logs', sa.Column('dashboard_id', sa.Integer(), nullable=True))
+ op.add_column('logs', sa.Column('slice_id', sa.Integer(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('logs', 'slice_id')
+ op.drop_column('logs', 'dashboard_id')
diff --git a/dashed/migrations/versions/43df8de3a5f4_dash_json.py b/dashed/migrations/versions/43df8de3a5f4_dash_json.py
new file mode 100644
index 000000000..c56ddc8f5
--- /dev/null
+++ b/dashed/migrations/versions/43df8de3a5f4_dash_json.py
@@ -0,0 +1,22 @@
+"""empty message
+
+Revision ID: 43df8de3a5f4
+Revises: 7dbf98566af7
+Create Date: 2016-01-18 23:43:16.073483
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '43df8de3a5f4'
+down_revision = '7dbf98566af7'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('dashboards', sa.Column('json_metadata', sa.Text(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('dashboards', 'json_metadata')
diff --git a/dashed/migrations/versions/4e6a06bad7a8_init.py b/dashed/migrations/versions/4e6a06bad7a8_init.py
new file mode 100644
index 000000000..3b8a9bff4
--- /dev/null
+++ b/dashed/migrations/versions/4e6a06bad7a8_init.py
@@ -0,0 +1,215 @@
+"""Init
+
+Revision ID: 4e6a06bad7a8
+Revises: None
+Create Date: 2015-09-21 17:30:38.442998
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '4e6a06bad7a8'
+down_revision = None
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('clusters',
+ 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('cluster_name', sa.String(length=250), nullable=True),
+ sa.Column('coordinator_host', sa.String(length=256), nullable=True),
+ sa.Column('coordinator_port', sa.Integer(), nullable=True),
+ sa.Column('coordinator_endpoint', sa.String(length=256), nullable=True),
+ sa.Column('broker_host', sa.String(length=256), nullable=True),
+ 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.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('cluster_name')
+ )
+ op.create_table('dashboards',
+ 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('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.PrimaryKeyConstraint('id')
+ )
+ op.create_table('dbs',
+ 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('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.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('database_name')
+ )
+ op.create_table('datasources',
+ 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('datasource_name', sa.String(length=250), nullable=True),
+ sa.Column('is_featured', sa.Boolean(), nullable=True),
+ 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.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('datasource_name')
+ )
+ op.create_table('tables',
+ 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_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.PrimaryKeyConstraint('id'),
+ sa.UniqueConstraint('table_name')
+ )
+ op.create_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('datasource_name', sa.String(length=250), nullable=True),
+ sa.Column('column_name', sa.String(length=256), 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),
+ sa.Column('count_distinct', sa.Boolean(), nullable=True),
+ sa.Column('sum', sa.Boolean(), nullable=True),
+ sa.Column('max', sa.Boolean(), nullable=True),
+ 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.PrimaryKeyConstraint('id')
+ )
+ op.create_table('metrics',
+ sa.Column('id', sa.Integer(), nullable=False),
+ 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('json', sa.Text(), nullable=True),
+ sa.Column('description', sa.Text(), nullable=True),
+ sa.ForeignKeyConstraint(['datasource_name'], ['datasources.datasource_name'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ op.create_table('slices',
+ 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('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('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.PrimaryKeyConstraint('id')
+ )
+ op.create_table('sql_metrics',
+ 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('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('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.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('column_name', sa.String(length=256), nullable=True),
+ sa.Column('is_dttm', sa.Boolean(), 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),
+ sa.Column('count_distinct', sa.Boolean(), nullable=True),
+ sa.Column('sum', sa.Boolean(), nullable=True),
+ sa.Column('max', sa.Boolean(), nullable=True),
+ 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.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.PrimaryKeyConstraint('id')
+ )
+ ### end Alembic commands ###
+
+
+def downgrade():
+ ### commands auto generated by Alembic - please adjust! ###
+ op.drop_table('dashboard_slices')
+ op.drop_table('table_columns')
+ op.drop_table('sql_metrics')
+ op.drop_table('slices')
+ op.drop_table('metrics')
+ op.drop_table('columns')
+ op.drop_table('tables')
+ op.drop_table('datasources')
+ op.drop_table('dbs')
+ op.drop_table('dashboards')
+ op.drop_table('clusters')
+ ### end Alembic commands ###
diff --git a/dashed/migrations/versions/55179c7f25c7_sqla_descr.py b/dashed/migrations/versions/55179c7f25c7_sqla_descr.py
new file mode 100644
index 000000000..aade0b930
--- /dev/null
+++ b/dashed/migrations/versions/55179c7f25c7_sqla_descr.py
@@ -0,0 +1,22 @@
+"""sqla_descr
+
+Revision ID: 55179c7f25c7
+Revises: 315b3f4da9b0
+Create Date: 2015-12-13 08:38:43.704145
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '55179c7f25c7'
+down_revision = '315b3f4da9b0'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('tables', sa.Column('description', sa.Text(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('tables', 'description')
diff --git a/dashed/migrations/versions/5a7bad26f2a7_.py b/dashed/migrations/versions/5a7bad26f2a7_.py
new file mode 100644
index 000000000..66dc20aae
--- /dev/null
+++ b/dashed/migrations/versions/5a7bad26f2a7_.py
@@ -0,0 +1,24 @@
+"""empty message
+
+Revision ID: 5a7bad26f2a7
+Revises: 4e6a06bad7a8
+Create Date: 2015-10-05 10:32:15.850753
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '5a7bad26f2a7'
+down_revision = '4e6a06bad7a8'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.add_column('dashboards', sa.Column('css', sa.Text(), nullable=True))
+ op.add_column('dashboards', sa.Column('description', sa.Text(), nullable=True))
+
+
+def downgrade():
+ op.drop_column('dashboards', 'description')
+ op.drop_column('dashboards', 'css')
diff --git a/dashed/migrations/versions/7dbf98566af7_slice_description.py b/dashed/migrations/versions/7dbf98566af7_slice_description.py
new file mode 100644
index 000000000..329af9ef2
--- /dev/null
+++ b/dashed/migrations/versions/7dbf98566af7_slice_description.py
@@ -0,0 +1,20 @@
+"""empty message
+
+Revision ID: 7dbf98566af7
+Revises: 8e80a26a31db
+Create Date: 2016-01-17 22:00:23.640788
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '7dbf98566af7'
+down_revision = '8e80a26a31db'
+
+from alembic import op
+import sqlalchemy as sa
+
+def upgrade():
+ op.add_column('slices', sa.Column('description', sa.Text(), nullable=True))
+
+def downgrade():
+ op.drop_column('slices', 'description')
diff --git a/dashed/migrations/versions/8e80a26a31db_.py b/dashed/migrations/versions/8e80a26a31db_.py
new file mode 100644
index 000000000..54edc58a8
--- /dev/null
+++ b/dashed/migrations/versions/8e80a26a31db_.py
@@ -0,0 +1,32 @@
+"""empty message
+
+Revision ID: 8e80a26a31db
+Revises: 2591d77e9831
+Create Date: 2016-01-13 20:24:45.256437
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = '8e80a26a31db'
+down_revision = '2591d77e9831'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.create_table('url',
+ 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('url', 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.PrimaryKeyConstraint('id')
+ )
+
+
+def downgrade():
+ op.drop_table('url')
diff --git a/dashed/migrations/versions/d827694c7555_css_templates.py b/dashed/migrations/versions/d827694c7555_css_templates.py
new file mode 100644
index 000000000..3b20e4405
--- /dev/null
+++ b/dashed/migrations/versions/d827694c7555_css_templates.py
@@ -0,0 +1,33 @@
+"""css templates
+
+Revision ID: d827694c7555
+Revises: 43df8de3a5f4
+Create Date: 2016-02-03 17:41:10.944019
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = 'd827694c7555'
+down_revision = '43df8de3a5f4'
+
+from alembic import op
+import sqlalchemy as sa
+
+
+def upgrade():
+ op.create_table('css_templates',
+ 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('template_name', sa.String(length=250), nullable=True),
+ sa.Column('css', sa.Text(), nullable=True),
+ sa.Column('changed_by_fk', sa.Integer(), nullable=True),
+ sa.Column('created_by_fk', sa.Integer(), nullable=True),
+ sa.ForeignKeyConstraint(['changed_by_fk'], ['ab_user.id'], ),
+ sa.ForeignKeyConstraint(['created_by_fk'], ['ab_user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+
+
+def downgrade():
+ op.drop_table('css_templates')
diff --git a/dashed/models.py b/dashed/models.py
new file mode 100644
index 000000000..1db3fd62d
--- /dev/null
+++ b/dashed/models.py
@@ -0,0 +1,1198 @@
+"""
+A collection of ORM sqlalchemy models for Dashed
+"""
+
+from copy import deepcopy, copy
+from collections import namedtuple
+from datetime import timedelta, datetime
+import functools
+import json
+import logging
+from six import string_types
+import sqlparse
+import requests
+
+from dateutil.parser import parse
+from flask import flash, request, g
+from flask.ext.appbuilder import Model
+from flask.ext.appbuilder.models.mixins import AuditMixin
+import pandas as pd
+from pydruid import client
+from pydruid.utils.filters import Dimension, Filter
+
+import sqlalchemy as sqla
+from sqlalchemy import (
+ Column, Integer, String, ForeignKey, Text, Boolean, DateTime,
+ Table, create_engine, MetaData, desc, select, and_, func)
+from sqlalchemy.engine import reflection
+from sqlalchemy.orm import relationship
+from sqlalchemy.sql import table, literal_column, text, column
+from sqlalchemy.sql.elements import ColumnClause
+from sqlalchemy_utils import EncryptedType
+
+from dashed import app, db, get_session, utils
+from dashed.viz import viz_types
+from sqlalchemy.ext.declarative import declared_attr
+
+config = app.config
+
+QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration'])
+
+
+class AuditMixinNullable(AuditMixin):
+
+ """Altering the AuditMixin to use nullable fields
+
+ Allows creating objects programmatically outside of CRUD
+ """
+
+ created_on = Column(DateTime, default=datetime.now, nullable=True)
+ changed_on = Column(
+ DateTime, default=datetime.now,
+ onupdate=datetime.now, nullable=True)
+
+ @declared_attr
+ def created_by_fk(cls):
+ return Column(Integer, ForeignKey('ab_user.id'),
+ default=cls.get_user_id, nullable=True)
+
+ @declared_attr
+ def changed_by_fk(cls):
+ return Column(Integer, ForeignKey('ab_user.id'),
+ default=cls.get_user_id, onupdate=cls.get_user_id, nullable=True)
+
+ @property
+ def created_by_(self):
+ return '{}'.format(self.created_by or '')
+
+ @property # noqa
+ def changed_by_(self):
+ return '{}'.format(self.changed_by or '')
+
+
+class Url(Model, AuditMixinNullable):
+
+ """Used for the short url feature"""
+
+ __tablename__ = 'url'
+ id = Column(Integer, primary_key=True)
+ url = Column(Text)
+
+
+class CssTemplate(Model, AuditMixinNullable):
+
+ """CSS templates for dashboards"""
+
+ __tablename__ = 'css_templates'
+ id = Column(Integer, primary_key=True)
+ template_name = Column(String(250))
+ css = Column(Text, default='')
+
+
+class Slice(Model, AuditMixinNullable):
+
+ """A slice is essentially a report or a view on data"""
+
+ __tablename__ = 'slices'
+ id = Column(Integer, primary_key=True)
+ slice_name = Column(String(250))
+ druid_datasource_id = Column(Integer, ForeignKey('datasources.id'))
+ table_id = Column(Integer, ForeignKey('tables.id'))
+ datasource_type = Column(String(200))
+ datasource_name = Column(String(2000))
+ viz_type = Column(String(250))
+ params = Column(Text)
+ description = Column(Text)
+
+ table = relationship(
+ 'SqlaTable', foreign_keys=[table_id], backref='slices')
+ druid_datasource = relationship(
+ 'DruidDatasource', foreign_keys=[druid_datasource_id], backref='slices')
+
+ def __repr__(self):
+ return self.slice_name
+
+ @property
+ def datasource(self):
+ return self.table or self.druid_datasource
+
+ @property
+ def datasource_link(self):
+ if self.table:
+ return self.table.link
+ elif self.druid_datasource:
+ return self.druid_datasource.link
+
+ @property
+ @utils.memoized
+ def viz(self):
+ d = json.loads(self.params)
+ viz = viz_types[self.viz_type](
+ self.datasource,
+ form_data=d)
+ return viz
+
+ @property
+ def description_markeddown(self):
+ return utils.markdown(self.description)
+
+ @property
+ def datasource_id(self):
+ return self.table_id or self.druid_datasource_id
+
+ @property
+ def data(self):
+ d = self.viz.data
+ d['slice_id'] = self.id
+ return d
+
+ @property
+ def json_data(self):
+ return json.dumps(self.data)
+
+ @property
+ def slice_url(self):
+ """Defines the url to access the slice"""
+ try:
+ slice_params = json.loads(self.params)
+ except Exception as e:
+ logging.exception(e)
+ slice_params = {}
+ slice_params['slice_id'] = self.id
+ slice_params['slice_name'] = self.slice_name
+ from werkzeug.urls import Href
+ href = Href(
+ "/dashed/explore/{self.datasource_type}/"
+ "{self.datasource_id}/".format(self=self))
+ return href(slice_params)
+
+ @property
+ def edit_url(self):
+ return "/slicemodelview/edit/{}".format(self.id)
+
+ @property
+ def slice_link(self):
+ url = self.slice_url
+ return '{self.slice_name} '.format(
+ url=url, self=self)
+
+
+dashboard_slices = Table('dashboard_slices', Model.metadata,
+ Column('id', Integer, primary_key=True),
+ Column('dashboard_id', Integer, ForeignKey('dashboards.id')),
+ Column('slice_id', Integer, ForeignKey('slices.id')),
+)
+
+
+class Dashboard(Model, AuditMixinNullable):
+
+ """The dashboard object!"""
+
+ __tablename__ = 'dashboards'
+ id = Column(Integer, primary_key=True)
+ dashboard_title = Column(String(500))
+ position_json = Column(Text)
+ description = Column(Text)
+ css = Column(Text)
+ json_metadata = Column(Text)
+ slug = Column(String(255), unique=True)
+ slices = relationship(
+ 'Slice', secondary=dashboard_slices, backref='dashboards')
+
+ def __repr__(self):
+ return self.dashboard_title
+
+ @property
+ def url(self):
+ return "/dashed/dashboard/{}/".format(self.slug or self.id)
+
+ @property
+ def metadata_dejson(self):
+ if self.json_metadata:
+ return json.loads(self.json_metadata)
+ else:
+ return {}
+
+ def dashboard_link(self):
+ return '{self.dashboard_title} '.format(self=self)
+
+ @property
+ def json_data(self):
+ d = {
+ 'id': self.id,
+ 'metadata': self.metadata_dejson,
+ 'dashboard_title': self.dashboard_title,
+ 'slug': self.slug,
+ 'slices': [slc.data for slc in self.slices],
+ }
+ return json.dumps(d)
+
+
+class Queryable(object):
+ """A common interface to objects that are queryable (tables and datasources)"""
+ @property
+ def column_names(self):
+ return sorted([c.column_name for c in self.columns])
+
+ @property
+ def main_dttm_col(self):
+ return "timestamp"
+
+ @property
+ def groupby_column_names(self):
+ return sorted([c.column_name for c in self.columns if c.groupby])
+
+ @property
+ def filterable_column_names(self):
+ return sorted([c.column_name for c in self.columns if c.filterable])
+
+ @property
+ def dttm_cols(self):
+ return []
+
+
+class Database(Model, AuditMixinNullable):
+
+ """An ORM object that stores Database related information"""
+
+ __tablename__ = 'dbs'
+ id = Column(Integer, primary_key=True)
+ database_name = Column(String(250), unique=True)
+ sqlalchemy_uri = Column(String(1024))
+ password = Column(EncryptedType(String(1024), config.get('SECRET_KEY')))
+
+ def __repr__(self):
+ return self.database_name
+
+ def get_sqla_engine(self):
+ return create_engine(self.sqlalchemy_uri_decrypted)
+
+ def safe_sqlalchemy_uri(self):
+ return self.sqlalchemy_uri
+
+ def grains(self):
+ """Defines time granularity database-specific expressions.
+
+ The idea here is to make it easy for users to change the time grain
+ form a datetime (maybe the source grain is arbitrary timestamps, daily
+ or 5 minutes increments) to another, "truncated" datetime. Since
+ each database has slightly different but similar datetime functions,
+ this allows a mapping between database engines and actual functions.
+ """
+ Grain = namedtuple('Grain', 'name function')
+ DB_TIME_GRAINS = {
+ 'presto': (
+ Grain('Time Column', '{col}'),
+ Grain('week', "date_trunc('week', CAST({col} AS DATE))"),
+ Grain('month', "date_trunc('month', CAST({col} AS DATE))"),
+ ),
+ 'mysql': (
+ Grain('Time Column', '{col}'),
+ Grain('day', 'DATE({col})'),
+ Grain('week', 'DATE_SUB({col}, INTERVAL DAYOFWEEK({col}) - 1 DAY)'),
+ Grain('month', 'DATE_SUB({col}, INTERVAL DAYOFMONTH({col}) - 1 DAY)'),
+ ),
+ }
+ for db_type, grains in DB_TIME_GRAINS.items():
+ if self.sqlalchemy_uri.startswith(db_type):
+ return grains
+
+ def grains_dict(self):
+ return {grain.name: grain for grain in self.grains()}
+
+ def get_table(self, table_name):
+ meta = MetaData()
+ return Table(
+ table_name, meta,
+ autoload=True,
+ autoload_with=self.get_sqla_engine())
+
+ def get_columns(self, table_name):
+ engine = self.get_sqla_engine()
+ insp = reflection.Inspector.from_engine(engine)
+ return insp.get_columns(table_name)
+
+ @property
+ def sqlalchemy_uri_decrypted(self):
+ conn = sqla.engine.url.make_url(self.sqlalchemy_uri)
+ conn.password = self.password
+ return str(conn)
+
+ @property
+ def sql_url(self):
+ return '/dashed/sql/{}/'.format(self.id)
+
+ @property
+ def sql_link(self):
+ return 'SQL '.format(self.sql_url)
+
+
+class SqlaTable(Model, Queryable, AuditMixinNullable):
+
+ """An ORM object for SqlAlchemy table references"""
+
+ type = "table"
+
+ __tablename__ = 'tables'
+ id = Column(Integer, primary_key=True)
+ table_name = Column(String(250), unique=True)
+ main_dttm_col = Column(String(250))
+ description = Column(Text)
+ default_endpoint = Column(Text)
+ database_id = Column(Integer, ForeignKey('dbs.id'), nullable=False)
+ is_featured = Column(Boolean, default=False)
+ user_id = Column(Integer, ForeignKey('ab_user.id'))
+ owner = relationship('User', backref='tables', foreign_keys=[user_id])
+ database = relationship(
+ 'Database', backref='tables', foreign_keys=[database_id])
+ offset = Column(Integer, default=0)
+
+ baselink = "tablemodelview"
+
+ def __repr__(self):
+ return self.table_name
+
+ @property
+ def description_markeddown(self):
+ return utils.markdown(self.description)
+
+ @property
+ def url(self):
+ return '/tablemodelview/edit/{}'.format(self.id)
+
+ @property
+ def link(self):
+ return '{self.table_name} '.format(**locals())
+
+ @property
+ def perm(self):
+ return (
+ "[{self.database}].[{self.table_name}]"
+ "(id:{self.id})").format(self=self)
+
+ @property
+ def full_name(self):
+ return "[{self.database}].[{self.table_name}]".format(self=self)
+
+ @property
+ def dttm_cols(self):
+ l = [c.column_name for c in self.columns if c.is_dttm]
+ if self.main_dttm_col not in l:
+ l.append(self.main_dttm_col)
+ return l
+
+ @property
+ def any_dttm_col(self):
+ cols = self.dttm_cols
+ if cols:
+ return cols[0]
+
+ @property
+ def html(self):
+ t = ((c.column_name, c.type) for c in self.columns)
+ df = pd.DataFrame(t)
+ df.columns = ['field', 'type']
+ return df.to_html(
+ index=False,
+ classes=(
+ "dataframe table table-striped table-bordered "
+ "table-condensed"))
+
+ @property
+ def name(self):
+ return self.table_name
+
+ @property
+ def table_link(self):
+ url = "/dashed/explore/{self.type}/{self.id}/".format(self=self)
+ return '{self.table_name} '.format(
+ url=url, self=self)
+
+ @property
+ def metrics_combo(self):
+ return sorted(
+ [
+ (m.metric_name, m.verbose_name or m.metric_name)
+ for m in self.metrics],
+ key=lambda x: x[1])
+
+ @property
+ def sql_url(self):
+ return self.database.sql_url + "?table_name=" + str(self.table_name)
+
+ @property
+ def sql_link(self):
+ return 'SQL '.format(self.sql_url)
+
+ def query(
+ self, groupby, metrics,
+ granularity,
+ from_dttm, to_dttm,
+ filter=None, # noqa
+ is_timeseries=True,
+ timeseries_limit=15, row_limit=None,
+ inner_from_dttm=None, inner_to_dttm=None,
+ extras=None,
+ columns=None):
+
+ # For backward compatibility
+ if granularity not in self.dttm_cols:
+ granularity = self.main_dttm_col
+
+ cols = {col.column_name: col for col in self.columns}
+ qry_start_dttm = datetime.now()
+
+ if not granularity and is_timeseries:
+ raise Exception(
+ "Datetime column not provided as part table configuration "
+ "and is required by this type of chart")
+
+ metrics_exprs = [
+ literal_column(m.expression).label(m.metric_name)
+ for m in self.metrics if m.metric_name in metrics]
+
+ if metrics:
+ main_metric_expr = literal_column([
+ m.expression for m in self.metrics
+ if m.metric_name == metrics[0]][0])
+ else:
+ main_metric_expr = literal_column("COUNT(*)")
+
+ select_exprs = []
+ groupby_exprs = []
+
+ if groupby:
+ select_exprs = []
+ inner_select_exprs = []
+ inner_groupby_exprs = []
+ for s in groupby:
+ col = cols[s]
+ expr = col.expression
+ if expr:
+ outer = literal_column(expr).label(s)
+ inner = literal_column(expr).label('__' + s)
+ else:
+ outer = column(s).label(s)
+ inner = column(s).label('__' + s)
+
+ groupby_exprs.append(outer)
+ select_exprs.append(outer)
+ inner_groupby_exprs.append(inner)
+ inner_select_exprs.append(inner)
+ elif columns:
+ for s in columns:
+ select_exprs.append(s)
+ metrics_exprs = []
+
+ if granularity:
+ dttm_expr = cols[granularity].expression or granularity
+ timestamp = literal_column(dttm_expr).label('timestamp')
+
+ # Transforming time grain into an expression based on configuration
+ time_grain_sqla = extras.get('time_grain_sqla')
+ if time_grain_sqla:
+ udf = self.database.grains_dict().get(time_grain_sqla, '{col}')
+ timestamp_grain = literal_column(
+ udf.function.format(col=dttm_expr)).label('timestamp')
+ else:
+ timestamp_grain = timestamp
+
+ if is_timeseries:
+ select_exprs += [timestamp_grain]
+ groupby_exprs += [timestamp_grain]
+
+ tf = '%Y-%m-%d %H:%M:%S.%f'
+ time_filter = [
+ timestamp >= from_dttm.strftime(tf),
+ timestamp <= to_dttm.strftime(tf),
+ ]
+ inner_time_filter = copy(time_filter)
+ if inner_from_dttm:
+ inner_time_filter[0] = timestamp >= inner_from_dttm.strftime(tf)
+ if inner_to_dttm:
+ inner_time_filter[1] = timestamp <= inner_to_dttm.strftime(tf)
+
+ select_exprs += metrics_exprs
+ qry = select(select_exprs)
+ from_clause = table(self.table_name)
+ if not columns:
+ qry = qry.group_by(*groupby_exprs)
+
+ where_clause_and = []
+ having_clause_and = []
+ for col, op, eq in filter:
+ col_obj = cols[col]
+ if op in ('in', 'not in'):
+ values = eq.split(",")
+ if col_obj.expression:
+ cond = ColumnClause(
+ col_obj.expression, is_literal=True).in_(values)
+ else:
+ cond = column(col).in_(values)
+ if op == 'not in':
+ cond = ~cond
+ where_clause_and.append(cond)
+ if extras and 'where' in extras:
+ where_clause_and += [text(extras['where'])]
+ if extras and 'having' in extras:
+ having_clause_and += [text(extras['having'])]
+ if granularity:
+ qry = qry.where(and_(*(time_filter + where_clause_and)))
+ qry = qry.having(and_(*having_clause_and))
+ if groupby:
+ qry = qry.order_by(desc(main_metric_expr))
+ qry = qry.limit(row_limit)
+
+ if timeseries_limit and groupby:
+ subq = select(inner_select_exprs)
+ subq = subq.select_from(table(self.table_name))
+ subq = subq.where(and_(*(where_clause_and + inner_time_filter)))
+ subq = subq.group_by(*inner_groupby_exprs)
+ subq = subq.order_by(desc(main_metric_expr))
+ subq = subq.limit(timeseries_limit)
+ on_clause = []
+ for i, gb in enumerate(groupby):
+ on_clause.append(
+ groupby_exprs[i] == column("__" + gb))
+
+ from_clause = from_clause.join(subq.alias(), and_(*on_clause))
+
+ qry = qry.select_from(from_clause)
+
+ engine = self.database.get_sqla_engine()
+ sql = "{}".format(
+ qry.compile(engine, compile_kwargs={"literal_binds": True}))
+ df = pd.read_sql_query(
+ sql=sql,
+ con=engine
+ )
+ sql = sqlparse.format(sql, reindent=True)
+ return QueryResult(
+ df=df, duration=datetime.now() - qry_start_dttm, query=sql)
+
+ 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:
+ flash(str(e))
+ flash(
+ "Table doesn't seem to exist in the specified database, "
+ "couldn't fetch column information", "danger")
+ return
+
+ TC = TableColumn
+ M = SqlMetric
+ metrics = []
+ any_date_col = None
+ for col in table.columns:
+ try:
+ datatype = str(col.type)
+ except Exception as e:
+ datatype = "UNKNOWN"
+ dbcol = (
+ db.session
+ .query(TC)
+ .filter(TC.table == self)
+ .filter(TC.column_name == col.name)
+ .first()
+ )
+ db.session.flush()
+ if not dbcol:
+ dbcol = TableColumn(column_name=col.name)
+
+ if (
+ str(datatype).startswith('VARCHAR') or
+ str(datatype).startswith('STRING')):
+ dbcol.groupby = True
+ dbcol.filterable = True
+ elif str(datatype).upper() in ('DOUBLE', 'FLOAT', 'INT', 'BIGINT'):
+ dbcol.sum = True
+ db.session.merge(self)
+ self.columns.append(dbcol)
+
+ if not any_date_col and 'date' in datatype.lower():
+ any_date_col = col.name
+
+ quoted = "{}".format(
+ column(dbcol.column_name).compile(dialect=db.engine.dialect))
+ if dbcol.sum:
+ metrics.append(M(
+ metric_name='sum__' + dbcol.column_name,
+ verbose_name='sum__' + dbcol.column_name,
+ metric_type='sum',
+ expression="SUM({})".format(quoted)
+ ))
+ if dbcol.max:
+ metrics.append(M(
+ metric_name='max__' + dbcol.column_name,
+ verbose_name='max__' + dbcol.column_name,
+ metric_type='max',
+ expression="MAX({})".format(quoted)
+ ))
+ if dbcol.min:
+ metrics.append(M(
+ metric_name='min__' + dbcol.column_name,
+ verbose_name='min__' + dbcol.column_name,
+ metric_type='min',
+ expression="MIN({})".format(quoted)
+ ))
+ if dbcol.count_distinct:
+ metrics.append(M(
+ metric_name='count_distinct__' + dbcol.column_name,
+ verbose_name='count_distinct__' + dbcol.column_name,
+ metric_type='count_distinct',
+ expression="COUNT(DISTINCT {})".format(quoted)
+ ))
+ dbcol.type = datatype
+ db.session.merge(self)
+ db.session.commit()
+
+ metrics.append(M(
+ metric_name='count',
+ verbose_name='COUNT(*)',
+ metric_type='count',
+ expression="COUNT(*)"
+ ))
+ for metric in metrics:
+ m = (
+ db.session.query(M)
+ .filter(M.metric_name == metric.metric_name)
+ .filter(M.table_id == self.id)
+ .first()
+ )
+ metric.table_id = self.id
+ if not m:
+ db.session.add(metric)
+ db.session.commit()
+ if not self.main_dttm_col:
+ self.main_dttm_col = any_date_col
+
+
+class SqlMetric(Model, AuditMixinNullable):
+
+ """ORM object for metrics, each table can have multiple metrics"""
+
+ __tablename__ = 'sql_metrics'
+ id = Column(Integer, primary_key=True)
+ metric_name = Column(String(512))
+ verbose_name = Column(String(1024))
+ metric_type = Column(String(32))
+ table_id = Column(Integer, ForeignKey('tables.id'))
+ table = relationship(
+ 'SqlaTable', backref='metrics', foreign_keys=[table_id])
+ expression = Column(Text)
+ description = Column(Text)
+
+
+class TableColumn(Model, AuditMixinNullable):
+
+ """ORM object for table columns, each table can have multiple columns"""
+
+ __tablename__ = 'table_columns'
+ id = Column(Integer, primary_key=True)
+ table_id = Column(Integer, ForeignKey('tables.id'))
+ table = relationship(
+ 'SqlaTable', backref='columns', foreign_keys=[table_id])
+ column_name = Column(String(256))
+ is_dttm = Column(Boolean, default=False)
+ is_active = Column(Boolean, default=True)
+ type = Column(String(32), default='')
+ groupby = Column(Boolean, default=False)
+ count_distinct = Column(Boolean, default=False)
+ sum = Column(Boolean, default=False)
+ max = Column(Boolean, default=False)
+ min = Column(Boolean, default=False)
+ filterable = Column(Boolean, default=False)
+ expression = Column(Text, default='')
+ description = Column(Text, default='')
+
+ def __repr__(self):
+ return self.column_name
+
+ @property
+ def isnum(self):
+ return self.type in ('LONG', 'DOUBLE', 'FLOAT')
+
+
+class DruidCluster(Model, AuditMixinNullable):
+
+ """ORM object referencing the Druid clusters"""
+
+ __tablename__ = 'clusters'
+ id = Column(Integer, primary_key=True)
+ cluster_name = Column(String(250), unique=True)
+ coordinator_host = Column(String(256))
+ coordinator_port = Column(Integer)
+ coordinator_endpoint = Column(
+ String(256), default='druid/coordinator/v1/metadata')
+ broker_host = Column(String(256))
+ broker_port = Column(Integer)
+ broker_endpoint = Column(String(256), default='druid/v2')
+ metadata_last_refreshed = Column(DateTime)
+
+ def __repr__(self):
+ return self.cluster_name
+
+ def get_pydruid_client(self):
+ cli = client.PyDruid(
+ "http://{0}:{1}/".format(self.broker_host, self.broker_port),
+ self.broker_endpoint)
+ return cli
+
+ def refresh_datasources(self):
+ endpoint = (
+ "http://{self.coordinator_host}:{self.coordinator_port}/"
+ "{self.coordinator_endpoint}/datasources"
+ ).format(self=self)
+
+ datasources = json.loads(requests.get(endpoint).text)
+ for datasource in datasources:
+ DruidDatasource.sync_to_db(datasource, self)
+
+
+class DruidDatasource(Model, AuditMixinNullable, Queryable):
+
+ """ORM object referencing Druid datasources (tables)"""
+
+ type = "druid"
+
+ baselink = "datasourcemodelview"
+
+ __tablename__ = 'datasources'
+ id = Column(Integer, primary_key=True)
+ datasource_name = Column(String(250), unique=True)
+ is_featured = Column(Boolean, default=False)
+ is_hidden = Column(Boolean, default=False)
+ description = Column(Text)
+ default_endpoint = Column(Text)
+ user_id = Column(Integer, ForeignKey('ab_user.id'))
+ owner = relationship('User', backref='datasources', foreign_keys=[user_id])
+ cluster_name = Column(
+ String(250), ForeignKey('clusters.cluster_name'))
+ cluster = relationship(
+ 'DruidCluster', backref='datasources', foreign_keys=[cluster_name])
+ offset = Column(Integer, default=0)
+
+ @property
+ def metrics_combo(self):
+ return sorted(
+ [(m.metric_name, m.verbose_name) for m in self.metrics],
+ key=lambda x: x[1])
+
+ @property
+ def name(self):
+ return self.datasource_name
+
+ @property
+ def perm(self):
+ return (
+ "[{self.cluster_name}].[{self.datasource_name}]"
+ "(id:{self.id})").format(self=self)
+
+ @property
+ def url(self):
+ return '/datasourcemodelview/edit/{}'.format(self.id)
+
+ @property
+ def link(self):
+ return (
+ ''
+ '{self.datasource_name} ').format(**locals())
+
+ @property
+ def full_name(self):
+ return (
+ "[{self.cluster_name}]."
+ "[{self.datasource_name}]").format(self=self)
+
+ def __repr__(self):
+ return self.datasource_name
+
+ @property
+ def datasource_link(self):
+ url = "/dashed/explore/{self.type}/{self.id}/".format(self=self)
+ return '{self.datasource_name} '.format(
+ url=url, self=self)
+
+ def get_metric_obj(self, metric_name):
+ return [
+ m.json_obj for m in self.metrics
+ if m.metric_name == metric_name
+ ][0]
+
+ def latest_metadata(self):
+ """Returns segment metadata from the latest segment"""
+ client = self.cluster.get_pydruid_client()
+ results = client.time_boundary(datasource=self.datasource_name)
+ if not results:
+ return
+ max_time = results[0]['result']['maxTime']
+ max_time = parse(max_time)
+ intervals = (max_time - timedelta(seconds=1)).isoformat() + '/'
+ intervals += (max_time + timedelta(seconds=1)).isoformat()
+ segment_metadata = client.segment_metadata(
+ datasource=self.datasource_name,
+ intervals=intervals)
+ if segment_metadata:
+ return segment_metadata[-1]['columns']
+
+ def generate_metrics(self):
+ for col in self.columns:
+ col.generate_metrics()
+
+ @classmethod
+ def sync_to_db(cls, name, cluster):
+ """Fetches metadata for that datasource and merges the Dashed db"""
+ print("Syncing Druid datasource [{}]".format(name))
+ session = get_session()
+ datasource = session.query(cls).filter_by(datasource_name=name).first()
+ if not datasource:
+ datasource = cls(datasource_name=name)
+ session.add(datasource)
+ flash("Adding new datasource [{}]".format(name), "success")
+ else:
+ flash("Refreshing datasource [{}]".format(name), "info")
+ datasource.cluster = cluster
+
+ cols = datasource.latest_metadata()
+ if not cols:
+ return
+ for col in cols:
+ col_obj = (
+ session
+ .query(DruidColumn)
+ .filter_by(datasource_name=name, column_name=col)
+ .first()
+ )
+ datatype = cols[col]['type']
+ if not col_obj:
+ col_obj = DruidColumn(datasource_name=name, column_name=col)
+ session.add(col_obj)
+ if datatype == "STRING":
+ col_obj.groupby = True
+ col_obj.filterable = True
+ if col_obj:
+ col_obj.type = cols[col]['type']
+ col_obj.datasource = datasource
+ col_obj.generate_metrics()
+
+ def query(
+ self, groupby, metrics,
+ granularity,
+ from_dttm, to_dttm,
+ filter=None, # noqa
+ is_timeseries=True,
+ timeseries_limit=None,
+ row_limit=None,
+ inner_from_dttm=None, inner_to_dttm=None,
+ extras=None, # noqa
+ select=None):
+ """Runs a query against Druid and returns a dataframe.
+
+ This query interface is common to SqlAlchemy and Druid
+ """
+ # TODO refactor into using a TBD Query object
+ qry_start_dttm = datetime.now()
+
+ inner_from_dttm = inner_from_dttm or from_dttm
+ inner_to_dttm = inner_to_dttm or to_dttm
+
+ # add tzinfo to native datetime with config
+ from_dttm = from_dttm.replace(tzinfo=config.get("DRUID_TZ"))
+ to_dttm = to_dttm.replace(tzinfo=config.get("DRUID_TZ"))
+
+ query_str = ""
+ aggregations = {
+ m.metric_name: m.json_obj
+ for m in self.metrics if m.metric_name in metrics
+ }
+ granularity = granularity or "all"
+ if granularity != "all":
+ granularity = utils.parse_human_timedelta(
+ granularity).total_seconds() * 1000
+ if not isinstance(granularity, string_types):
+ granularity = {"type": "duration", "duration": granularity}
+
+ qry = dict(
+ datasource=self.datasource_name,
+ dimensions=groupby,
+ aggregations=aggregations,
+ granularity=granularity,
+ intervals=from_dttm.isoformat() + '/' + to_dttm.isoformat(),
+ )
+ filters = None
+ for col, op, eq in filter:
+ cond = None
+ if op == '==':
+ cond = Dimension(col) == eq
+ elif op == '!=':
+ cond = ~(Dimension(col) == eq)
+ elif op in ('in', 'not in'):
+ fields = []
+ splitted = eq.split(',')
+ if len(splitted) > 1:
+ for s in eq.split(','):
+ s = s.strip()
+ fields.append(Filter.build_filter(Dimension(col) == s))
+ cond = Filter(type="or", fields=fields)
+ else:
+ cond = Dimension(col) == eq
+ if op == 'not in':
+ cond = ~cond
+ if filters:
+ filters = Filter(type="and", fields=[
+ Filter.build_filter(cond),
+ Filter.build_filter(filters)
+ ])
+ else:
+ filters = cond
+
+ if filters:
+ qry['filter'] = filters
+
+ client = self.cluster.get_pydruid_client()
+ orig_filters = filters
+ if timeseries_limit and is_timeseries:
+ # Limit on the number of timeseries, doing a two-phases query
+ pre_qry = deepcopy(qry)
+ pre_qry['granularity'] = "all"
+ pre_qry['limit_spec'] = {
+ "type": "default",
+ "limit": timeseries_limit,
+ 'intervals': (
+ inner_from_dttm.isoformat() + '/' +
+ inner_to_dttm.isoformat()),
+ "columns": [{
+ "dimension": metrics[0] if metrics else self.metrics[0],
+ "direction": "descending",
+ }],
+ }
+ client.groupby(**pre_qry)
+ query_str += "// Two phase query\n// Phase 1\n"
+ query_str += json.dumps(client.query_dict, indent=2) + "\n"
+ query_str += "//\nPhase 2 (built based on phase one's results)\n"
+ df = client.export_pandas()
+ if df is not None and not df.empty:
+ dims = qry['dimensions']
+ filters = []
+ for _, row in df.iterrows():
+ fields = []
+ for dim in dims:
+ f = Filter.build_filter(Dimension(dim) == row[dim])
+ fields.append(f)
+ if len(fields) > 1:
+ filt = Filter(type="and", fields=fields)
+ filters.append(Filter.build_filter(filt))
+ elif fields:
+ filters.append(fields[0])
+
+ if filters:
+ ff = Filter(type="or", fields=filters)
+ if not orig_filters:
+ qry['filter'] = ff
+ else:
+ qry['filter'] = Filter(type="and", fields=[
+ Filter.build_filter(ff),
+ Filter.build_filter(orig_filters)])
+ qry['limit_spec'] = None
+ if row_limit:
+ qry['limit_spec'] = {
+ "type": "default",
+ "limit": row_limit,
+ "columns": [{
+ "dimension": metrics[0] if metrics else self.metrics[0],
+ "direction": "descending",
+ }],
+ }
+ client.groupby(**qry)
+ query_str += json.dumps(client.query_dict, indent=2)
+ df = client.export_pandas()
+ if df is None or df.size == 0:
+ raise Exception("No data was returned.")
+
+ if (
+ not is_timeseries and
+ granularity == "all" and
+ 'timestamp' in df.columns):
+ del df['timestamp']
+
+ # Reordering columns
+ cols = []
+ if 'timestamp' in df.columns:
+ cols += ['timestamp']
+ cols += [col for col in groupby if col in df.columns]
+ cols += [col for col in metrics if col in df.columns]
+ cols += [col for col in df.columns if col not in cols]
+ df = df[cols]
+ return QueryResult(
+ df=df,
+ query=query_str,
+ duration=datetime.now() - qry_start_dttm)
+
+
+class Log(Model):
+
+ """ORM object used to log Dashed actions to the database"""
+
+ __tablename__ = 'logs'
+
+ id = Column(Integer, primary_key=True)
+ action = Column(String(512))
+ user_id = Column(Integer, ForeignKey('ab_user.id'))
+ dashboard_id = Column(Integer)
+ slice_id = Column(Integer)
+ user_id = Column(Integer, ForeignKey('ab_user.id'))
+ json = Column(Text)
+ user = relationship('User', backref='logs', foreign_keys=[user_id])
+ dttm = Column(DateTime, default=func.now())
+
+ @classmethod
+ def log_this(cls, f):
+ """Decorator to log user actions"""
+ @functools.wraps(f)
+ def wrapper(*args, **kwargs):
+ user_id = None
+ if g.user:
+ user_id = g.user.id
+ d = request.args.to_dict()
+ d.update(kwargs)
+ log = cls(
+ action=f.__name__,
+ json=json.dumps(d),
+ dashboard_id=d.get('dashboard_id') or None,
+ slice_id=d.get('slice_id') or None,
+ user_id=user_id)
+ db.session.add(log)
+ db.session.commit()
+ return f(*args, **kwargs)
+ return wrapper
+
+
+
+
+class DruidMetric(Model):
+
+ """ORM object referencing Druid metrics for a datasource"""
+
+ __tablename__ = 'metrics'
+ id = Column(Integer, primary_key=True)
+ metric_name = Column(String(512))
+ verbose_name = Column(String(1024))
+ metric_type = Column(String(32))
+ datasource_name = Column(
+ String(250),
+ ForeignKey('datasources.datasource_name'))
+ datasource = relationship('DruidDatasource', backref='metrics')
+ json = Column(Text)
+ description = Column(Text)
+
+ @property
+ def json_obj(self):
+ try:
+ obj = json.loads(self.json)
+ except Exception:
+ obj = {}
+ return obj
+
+
+class DruidColumn(Model):
+
+ """ORM model for storing Druid datasource column metadata"""
+
+ __tablename__ = 'columns'
+ id = Column(Integer, primary_key=True)
+ datasource_name = Column(
+ String(250),
+ ForeignKey('datasources.datasource_name'))
+ datasource = relationship('DruidDatasource', backref='columns')
+ column_name = Column(String(256))
+ is_active = Column(Boolean, default=True)
+ type = Column(String(32))
+ groupby = Column(Boolean, default=False)
+ count_distinct = Column(Boolean, default=False)
+ sum = Column(Boolean, default=False)
+ max = Column(Boolean, default=False)
+ min = Column(Boolean, default=False)
+ filterable = Column(Boolean, default=False)
+ description = Column(Text)
+
+ def __repr__(self):
+ return self.column_name
+
+ @property
+ def isnum(self):
+ return self.type in ('LONG', 'DOUBLE', 'FLOAT')
+
+ def generate_metrics(self):
+ """Generate metrics based on the column metadata"""
+ M = DruidMetric
+ metrics = []
+ metrics.append(DruidMetric(
+ metric_name='count',
+ verbose_name='COUNT(*)',
+ metric_type='count',
+ json=json.dumps({'type': 'count', 'name': 'count'})
+ ))
+ # Somehow we need to reassign this for UDAFs
+ if self.type in ('DOUBLE', 'FLOAT'):
+ corrected_type = 'DOUBLE'
+ else:
+ corrected_type = self.type
+
+ if self.sum and self.isnum:
+ mt = corrected_type.lower() + 'Sum'
+ name = 'sum__' + self.column_name
+ metrics.append(DruidMetric(
+ metric_name=name,
+ metric_type='sum',
+ verbose_name='SUM({})'.format(self.column_name),
+ json=json.dumps({
+ 'type': mt, 'name': name, 'fieldName': self.column_name})
+ ))
+ if self.min and self.isnum:
+ mt = corrected_type.lower() + 'Min'
+ name = 'min__' + self.column_name
+ metrics.append(DruidMetric(
+ metric_name=name,
+ metric_type='min',
+ verbose_name='MIN({})'.format(self.column_name),
+ json=json.dumps({
+ 'type': mt, 'name': name, 'fieldName': self.column_name})
+ ))
+ if self.max and self.isnum:
+ mt = corrected_type.lower() + 'Max'
+ name = 'max__' + self.column_name
+ metrics.append(DruidMetric(
+ metric_name=name,
+ metric_type='max',
+ verbose_name='MAX({})'.format(self.column_name),
+ json=json.dumps({
+ 'type': mt, 'name': name, 'fieldName': self.column_name})
+ ))
+ if self.count_distinct:
+ mt = 'count_distinct'
+ name = 'count_distinct__' + self.column_name
+ metrics.append(DruidMetric(
+ metric_name=name,
+ verbose_name='COUNT(DISTINCT {})'.format(self.column_name),
+ metric_type='count_distinct',
+ json=json.dumps({
+ 'type': 'cardinality',
+ 'name': name,
+ 'fieldNames': [self.column_name]})
+ ))
+ session = get_session()
+ for metric in metrics:
+ m = (
+ session.query(M)
+ .filter(M.metric_name == metric.metric_name)
+ .filter(M.datasource_name == self.datasource_name)
+ .filter(DruidCluster.cluster_name == self.datasource.cluster_name)
+ .first()
+ )
+ metric.datasource_name = self.datasource_name
+ if not m:
+ session.add(metric)
+ session.commit()
diff --git a/dashed/static/assets b/dashed/static/assets
new file mode 120000
index 000000000..ec2e4be2f
--- /dev/null
+++ b/dashed/static/assets
@@ -0,0 +1 @@
+../assets
\ No newline at end of file
diff --git a/dashed/static/docs b/dashed/static/docs
new file mode 120000
index 000000000..932170420
--- /dev/null
+++ b/dashed/static/docs
@@ -0,0 +1 @@
+../../docs/_build/html/
\ No newline at end of file
diff --git a/dashed/static/favicon.png b/dashed/static/favicon.png
new file mode 100644
index 000000000..50c8c9a45
Binary files /dev/null and b/dashed/static/favicon.png differ
diff --git a/dashed/static/img/bubble.png b/dashed/static/img/bubble.png
new file mode 100644
index 000000000..a65d5ed8b
Binary files /dev/null and b/dashed/static/img/bubble.png differ
diff --git a/dashed/static/img/cardash.jpg b/dashed/static/img/cardash.jpg
new file mode 100644
index 000000000..e8dbcc49c
Binary files /dev/null and b/dashed/static/img/cardash.jpg differ
diff --git a/dashed/static/img/cloud.png b/dashed/static/img/cloud.png
new file mode 100644
index 000000000..9478806cf
Binary files /dev/null and b/dashed/static/img/cloud.png differ
diff --git a/dashed/static/img/dash.png b/dashed/static/img/dash.png
new file mode 100644
index 000000000..83ecf8e57
Binary files /dev/null and b/dashed/static/img/dash.png differ
diff --git a/dashed/static/img/dashed_screenshot.png b/dashed/static/img/dashed_screenshot.png
new file mode 100644
index 000000000..804576455
Binary files /dev/null and b/dashed/static/img/dashed_screenshot.png differ
diff --git a/dashed/static/img/favicon.png b/dashed/static/img/favicon.png
new file mode 100644
index 000000000..35fc3c161
Binary files /dev/null and b/dashed/static/img/favicon.png differ
diff --git a/dashed/static/img/gallery.jpg b/dashed/static/img/gallery.jpg
new file mode 100644
index 000000000..42ebad239
Binary files /dev/null and b/dashed/static/img/gallery.jpg differ
diff --git a/dashed/static/img/loading.gif b/dashed/static/img/loading.gif
new file mode 100644
index 000000000..01ae3939c
Binary files /dev/null and b/dashed/static/img/loading.gif differ
diff --git a/dashed/static/img/penguins.png b/dashed/static/img/penguins.png
new file mode 100644
index 000000000..14bfc440e
Binary files /dev/null and b/dashed/static/img/penguins.png differ
diff --git a/dashed/static/img/serpe.jpg b/dashed/static/img/serpe.jpg
new file mode 100644
index 000000000..79a91666a
Binary files /dev/null and b/dashed/static/img/serpe.jpg differ
diff --git a/dashed/static/img/servers.jpg b/dashed/static/img/servers.jpg
new file mode 100644
index 000000000..2d4604b6b
Binary files /dev/null and b/dashed/static/img/servers.jpg differ
diff --git a/dashed/static/img/slice.jpg b/dashed/static/img/slice.jpg
new file mode 100644
index 000000000..68c53c4e8
Binary files /dev/null and b/dashed/static/img/slice.jpg differ
diff --git a/dashed/templates/appbuilder/baselayout.html b/dashed/templates/appbuilder/baselayout.html
new file mode 100644
index 000000000..33110ce16
--- /dev/null
+++ b/dashed/templates/appbuilder/baselayout.html
@@ -0,0 +1,40 @@
+{% extends 'appbuilder/init.html' %}
+{% import 'appbuilder/baselib.html' as baselib %}
+
+{% block body %}
+ {% include 'appbuilder/general/confirm.html' %}
+ {% include 'appbuilder/general/alert.html' %}
+
+ {% block navbar %}
+
+ {% include 'appbuilder/navbar.html' %}
+
+ {% endblock %}
+
+ {% block uncontained %}{% endblock %}
+
+
+
+ {% block messages %}
+ {% include 'appbuilder/flash.html' %}
+ {% endblock %}
+ {% block content %}
+ {% endblock %}
+
+
+
+ {% block content_fluid %}
+ {% endblock %}
+
+
+ {% block footer %}
+
+ {% endblock %}
+{% endblock %}
+
diff --git a/dashed/templates/appbuilder/general/widgets/list.html b/dashed/templates/appbuilder/general/widgets/list.html
new file mode 100644
index 000000000..b0c28eda9
--- /dev/null
+++ b/dashed/templates/appbuilder/general/widgets/list.html
@@ -0,0 +1,81 @@
+{% import 'appbuilder/general/lib.html' as lib %}
+{% extends 'appbuilder/general/widgets/base_list.html' %}
+
+
+ {% block begin_content scoped %}
+
+ {% endblock %}
+
diff --git a/dashed/templates/appbuilder/navbar.html b/dashed/templates/appbuilder/navbar.html
new file mode 100644
index 000000000..d0d260c2c
--- /dev/null
+++ b/dashed/templates/appbuilder/navbar.html
@@ -0,0 +1,35 @@
+
+
+
+{% set menu = appbuilder.menu %}
+{% set languages = appbuilder.languages %}
+
+
diff --git a/dashed/templates/appbuilder/navbar_right.html b/dashed/templates/appbuilder/navbar_right.html
new file mode 100644
index 000000000..433bf1bf4
--- /dev/null
+++ b/dashed/templates/appbuilder/navbar_right.html
@@ -0,0 +1,43 @@
+
+{% macro locale_menu(languages) %}
+{% set locale = session['locale'] %}
+{% if not locale %}
+ {% set locale = 'en' %}
+{% endif %}
+
+
+
+
+
+ {% if languages.keys()|length > 1 %}
+
+ {% endif %}
+
+{% endmacro %}
+
+
+
+{% if not current_user.is_anonymous() %}
+
+
+ {{g.user.get_full_name()}}
+
+
+
+{% else %}
+
+ {{_("Login")}}
+{% endif %}
diff --git a/dashed/templates/dashed/ajah.html b/dashed/templates/dashed/ajah.html
new file mode 100644
index 000000000..b5d122680
--- /dev/null
+++ b/dashed/templates/dashed/ajah.html
@@ -0,0 +1 @@
+{{ content |safe }}
diff --git a/dashed/templates/dashed/base.html b/dashed/templates/dashed/base.html
new file mode 100644
index 000000000..da9e1cee9
--- /dev/null
+++ b/dashed/templates/dashed/base.html
@@ -0,0 +1,12 @@
+{% extends "appbuilder/baselayout.html" %}
+
+ {% block head_css %}
+ {{super()}}
+
+
+ {% endblock %}
+
+ {% block head_js %}
+ {{super()}}
+
+ {% endblock %}
diff --git a/dashed/templates/dashed/basic.html b/dashed/templates/dashed/basic.html
new file mode 100644
index 000000000..8d0cba178
--- /dev/null
+++ b/dashed/templates/dashed/basic.html
@@ -0,0 +1,36 @@
+
+
+
+ {% block title %}
+ {% if appbuilder and appbuilder.app_name %} {{ appbuilder.app_name }} {% endif %}
+ {% endblock %}
+
+ {% block head_meta %}{% endblock %}
+ {% block head_css %}
+
+
+ {% endblock %}
+ {% block head_js %}
+
+ {% endblock %}
+
+
+
+ {% block navbar %}
+ {% if not viz or viz.request.args.get("standalone") != "true" %}
+
+ {% include 'appbuilder/navbar.html' %}
+
+ {% endif %}
+ {% endblock %}
+
+ {% block body %}
+
+ Oops! React.js is not working properly.
+
+ {% endblock %}
+
+ {% block tail_js %}
+ {% endblock %}
+
+
diff --git a/dashed/templates/dashed/dashboard.html b/dashed/templates/dashed/dashboard.html
new file mode 100644
index 000000000..905435cfe
--- /dev/null
+++ b/dashed/templates/dashed/dashboard.html
@@ -0,0 +1,139 @@
+{% extends "dashed/basic.html" %}
+
+{% block head_js %}
+ {{ super() }}
+
+{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+
+
+
+ CSS template
+ {% for t in templates %}
+
+ {{ t.template_name }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ dashboard.dashboard_title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% for slice in dashboard.slices %}
+ {% set pos = pos_dict.get(slice.id, {}) %}
+ {% set viz = slice.viz %}
+
+
+
+
+
+
+
+ {{ slice.description_markeddown | safe }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/dashed/templates/dashed/explore.html b/dashed/templates/dashed/explore.html
new file mode 100644
index 000000000..106bd487a
--- /dev/null
+++ b/dashed/templates/dashed/explore.html
@@ -0,0 +1,212 @@
+{% extends "dashed/basic.html" %}
+
+{% block body %}
+ {% set datasource = viz.datasource %}
+ {% set form = viz.form %}
+
+ {% macro panofield(fieldname)%}
+
+ {% set field = form.get_field(fieldname)%}
+
+ {{ viz.get_form_override(fieldname, 'label') or field.label }}
+ {% if field.description %}
+
+ {% endif %}
+ {{ field(class_=form.field_css_classes(field.name)) }}
+
+
+ {% endmacro %}
+
+
+{% endblock %}
+
+{% block tail_js %}
+ {{ super() }}
+
+{% endblock %}
diff --git a/dashed/templates/dashed/featured.html b/dashed/templates/dashed/featured.html
new file mode 100644
index 000000000..8b41f6c9b
--- /dev/null
+++ b/dashed/templates/dashed/featured.html
@@ -0,0 +1,42 @@
+{% extends "dashed/basic.html" %}
+
+{% block head_js %}
+ {{ super() }}
+
+{% endblock %}
+
+{% block body %}
+
+
+
+
+
+
+ Table
+ Database
+ Owner
+
+
+
+
+ {% for dataset in featured_datasets %}
+
+
+
+
{{ dataset.table_name }}
+
{{ utils.markdown(dataset.description) | safe }}
+
+
+ {{ dataset.database }}
+ {{ dataset.owner or '' }}
+
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
+
diff --git a/dashed/templates/dashed/index.html b/dashed/templates/dashed/index.html
new file mode 100644
index 000000000..71ed52780
--- /dev/null
+++ b/dashed/templates/dashed/index.html
@@ -0,0 +1,6 @@
+{% extends "dashed/basic.html" %}
+
+{% block tail_js %}
+ {{ super() }}
+
+{% endblock %}
diff --git a/dashed/templates/dashed/models/database/add.html b/dashed/templates/dashed/models/database/add.html
new file mode 100644
index 000000000..79b6cf6cb
--- /dev/null
+++ b/dashed/templates/dashed/models/database/add.html
@@ -0,0 +1,7 @@
+{% extends "appbuilder/general/model/add.html" %}
+
+{% import "dashed/models/database/macros.html" as macros %}
+{% block tail_js %}
+ {{ super() }}
+ {{ macros.testconn() }}
+{% endblock %}
diff --git a/dashed/templates/dashed/models/database/edit.html b/dashed/templates/dashed/models/database/edit.html
new file mode 100644
index 000000000..b1db50f2b
--- /dev/null
+++ b/dashed/templates/dashed/models/database/edit.html
@@ -0,0 +1,7 @@
+{% extends "appbuilder/general/model/edit.html" %}
+
+{% import "dashed/models/database/macros.html" as macros %}
+{% block tail_js %}
+ {{ super() }}
+ {{ macros.testconn() }}
+{% endblock %}
diff --git a/dashed/templates/dashed/models/database/macros.html b/dashed/templates/dashed/models/database/macros.html
new file mode 100644
index 000000000..dac8b6d50
--- /dev/null
+++ b/dashed/templates/dashed/models/database/macros.html
@@ -0,0 +1,29 @@
+{% macro testconn() %}
+
+{% endmacro %}
diff --git a/dashed/templates/dashed/no_data.html b/dashed/templates/dashed/no_data.html
new file mode 100644
index 000000000..a8de807a6
--- /dev/null
+++ b/dashed/templates/dashed/no_data.html
@@ -0,0 +1,5 @@
+{% extends "dashed/datasource.html" %}
+
+{% block viz %}
+No data: review your incantations.
+{% endblock %}
diff --git a/dashed/templates/dashed/sql.html b/dashed/templates/dashed/sql.html
new file mode 100644
index 000000000..fd660eac3
--- /dev/null
+++ b/dashed/templates/dashed/sql.html
@@ -0,0 +1,99 @@
+{% extends "dashed/basic.html" %}
+
+{% block head_css %}
+ {{super()}}
+
+
+
+{% endblock %}
+
+{% block body %}
+
+
+
db: [{{ db }}]
+
+
+
+ Run!
+ Create View
+
+
+
+ {% for t in tables %}
+
+ {{ t }}
+
+ {% endfor %}
+
+ SELECT *
+
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block tail_js %}
+{{ super() }}
+
+{% endblock %}
diff --git a/dashed/templates/dashed/standalone.html b/dashed/templates/dashed/standalone.html
new file mode 100644
index 000000000..319aad13c
--- /dev/null
+++ b/dashed/templates/dashed/standalone.html
@@ -0,0 +1,21 @@
+
+
+
+
+ {% set CSS_THEME = appbuilder.get_app.config.get("CSS_THEME") %}
+ {% set height = request.args.get("height", 700) %}
+ {% if CSS_THEME %}
+
+ {% endif %}
+
+
+
+
+
diff --git a/dashed/templates/dashed/traceback.html b/dashed/templates/dashed/traceback.html
new file mode 100644
index 000000000..97fc6b496
--- /dev/null
+++ b/dashed/templates/dashed/traceback.html
@@ -0,0 +1,10 @@
+
+
+
+ {{ art }}
+{{ title }}
+{{ error_msg }}
+
+
+
+
diff --git a/dashed/templates/dashed/viz.html b/dashed/templates/dashed/viz.html
new file mode 100644
index 000000000..ea1dc7fc0
--- /dev/null
+++ b/dashed/templates/dashed/viz.html
@@ -0,0 +1,11 @@
+{% if viz.form_data.get("json") == "true" %}
+ {{ viz.get_json() }}
+{% else %}
+
+ {% if viz.request.args.get("standalone") == "true" %}
+ {% extends 'dashed/standalone.html' %}
+ {% else %}
+ {% extends 'dashed/explore.html' %}
+ {% endif %}
+
+{% endif %}
diff --git a/dashed/templates/index.html b/dashed/templates/index.html
new file mode 100644
index 000000000..e2f16ba7e
--- /dev/null
+++ b/dashed/templates/index.html
@@ -0,0 +1,116 @@
+{% extends "appbuilder/base.html" %}
+
+{% block uncontained %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashed
+
+ an open source data visualization platform
+
+
+
+
+
+
+
+
Explore your data
+
+
+ Intuitively navigate your data while slicing, dicing, and
+ visualizing through a rich set of widgets
+
+
+
+
+
+
+
Create and share dashboards
+
Assemble many data visualization "slices" into a rich collection
+
+
+
+
+
+
Extend
+
Join the community and take part in extending the widget library
+
+
+
+
+
+
Connect
+
+ Access data from MySql, Presto.db, Postgres, RedShift, Oracle, MsSql,
+ SQLite, and more through the SqlAlchemy integration. You can also
+ query realtime data blazingly fast out of Druid.io
+
+
+
+
+
+
+
+
+
+
+
+
+
Dashboards
+
Browse the dashboards list
+
+
+
+
+
+
+
Slices
+
"Slices" are individual views into a single dataset
+
+
+
+
+
+
+
Gallery
+
Navigate through the growing set of visualizations
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block tail_js %}
+{{ super() }}
+
+{% endblock %}
diff --git a/dashed/utils.py b/dashed/utils.py
new file mode 100644
index 000000000..dee2cdd9b
--- /dev/null
+++ b/dashed/utils.py
@@ -0,0 +1,248 @@
+"""Utility functions used across Dashed"""
+
+from datetime import datetime
+import hashlib
+import functools
+import json
+import logging
+
+from dateutil.parser import parse
+from sqlalchemy.types import TypeDecorator, TEXT
+from markdown import markdown as md
+import parsedatetime
+from flask_appbuilder.security.sqla import models as ab_models
+
+
+class memoized(object):
+
+ """Decorator that caches a function's return value each time it is called.
+ If called later with the same arguments, the cached value is returned, and
+ not re-evaluated.
+ """
+
+ def __init__(self, func):
+ self.func = func
+ self.cache = {}
+ def __call__(self, *args):
+ try:
+ return self.cache[args]
+ except KeyError:
+ value = self.func(*args)
+ self.cache[args] = value
+ return value
+ except TypeError:
+ # uncachable -- for instance, passing a list as an argument.
+ # Better to not cache than to blow up entirely.
+ return self.func(*args)
+ def __repr__(self):
+ """Return the function's docstring."""
+ return self.func.__doc__
+ def __get__(self, obj, objtype):
+ """Support instance methods."""
+ return functools.partial(self.__call__, obj)
+
+def list_minus(l, minus):
+ """Returns l without what is in minus
+
+ >>> list_minus([1, 2, 3], [2])
+ [1, 3]
+ """
+ return [o for o in l if o not in minus]
+
+def parse_human_datetime(s):
+ """
+ Returns ``datetime.datetime`` from human readable strings
+
+ >>> from datetime import date, timedelta
+ >>> from dateutil.relativedelta import relativedelta
+ >>> parse_human_datetime('2015-04-03')
+ datetime.datetime(2015, 4, 3, 0, 0)
+ >>> parse_human_datetime('2/3/1969')
+ datetime.datetime(1969, 2, 3, 0, 0)
+ >>> parse_human_datetime("now") <= datetime.now()
+ True
+ >>> parse_human_datetime("yesterday") <= datetime.now()
+ True
+ >>> date.today() - timedelta(1) == parse_human_datetime('yesterday').date()
+ True
+ >>> year_ago_1 = parse_human_datetime('one year ago').date()
+ >>> year_ago_2 = (datetime.now() - relativedelta(years=1) ).date()
+ >>> year_ago_1 == year_ago_2
+ True
+ """
+ try:
+ dttm = parse(s)
+ except Exception:
+ try:
+ cal = parsedatetime.Calendar()
+ dttm = dttm_from_timtuple(cal.parse(s)[0])
+ except Exception as e:
+ logging.exception(e)
+ raise ValueError("Couldn't parse date string [{}]".format(s))
+ return dttm
+
+
+def dttm_from_timtuple(d):
+ return datetime(
+ d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
+
+
+def merge_perm(sm, permission_name, view_menu_name):
+ pv = sm.find_permission_view_menu(permission_name, view_menu_name)
+ if not pv:
+ sm.add_permission_view_menu(permission_name, view_menu_name)
+
+
+def parse_human_timedelta(s):
+ """
+ Returns ``datetime.datetime`` from natural language time deltas
+
+ >>> parse_human_datetime("now") <= datetime.now()
+ True
+ """
+ cal = parsedatetime.Calendar()
+ dttm = dttm_from_timtuple(datetime.now().timetuple())
+ d = cal.parse(s, dttm)[0]
+ d = datetime(
+ d.tm_year, d.tm_mon, d.tm_mday, d.tm_hour, d.tm_min, d.tm_sec)
+ return d - dttm
+
+
+class JSONEncodedDict(TypeDecorator):
+
+ """Represents an immutable structure as a json-encoded string."""
+
+ impl = TEXT
+ def process_bind_param(self, value, dialect):
+ if value is not None:
+ value = json.dumps(value)
+
+ return value
+
+ def process_result_value(self, value, dialect):
+ if value is not None:
+ value = json.loads(value)
+ return value
+
+
+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(dashed):
+ """Inits the Dashed application with security roles and such"""
+ db = dashed.db
+ models = dashed.models
+ sm = dashed.appbuilder.sm
+ alpha = sm.add_role("Alpha")
+ admin = sm.add_role("Admin")
+
+ merge_perm(sm, 'all_datasource_access', 'all_datasource_access')
+
+ perms = db.session.query(ab_models.PermissionView).all()
+ for perm in perms:
+ if perm.permission.name == 'datasource_access':
+ continue
+ if perm.view_menu.name not in (
+ 'UserDBModelView', 'RoleModelView', 'ResetPasswordView',
+ 'Security'):
+ sm.add_permission_role(alpha, perm)
+ sm.add_permission_role(admin, perm)
+ gamma = sm.add_role("Gamma")
+ for perm in perms:
+ if(
+ perm.view_menu.name not in (
+ 'ResetPasswordView',
+ 'RoleModelView',
+ 'UserDBModelView',
+ 'Security') and
+ perm.permission.name not in (
+ 'all_datasource_access',
+ 'can_add',
+ 'can_download',
+ 'can_delete',
+ 'can_edit',
+ 'can_save',
+ 'datasource_access',
+ 'muldelete',
+ )):
+ sm.add_permission_role(gamma, perm)
+ session = db.session()
+ table_perms = [
+ table.perm for table in session.query(models.SqlaTable).all()]
+ table_perms += [
+ table.perm for table in session.query(models.DruidDatasource).all()]
+ for table_perm in table_perms:
+ merge_perm(sm, 'datasource_access', table_perm)
+
+
+def datetime_f(dttm):
+ """Formats datetime to take less room when it is recent"""
+ if dttm:
+ dttm = dttm.isoformat()
+ now_iso = datetime.now().isoformat()
+ if now_iso[:10] == dttm[:10]:
+ dttm = dttm[11:]
+ elif now_iso[:4] == dttm[:4]:
+ dttm = dttm[5:]
+ return "{} ".format(dttm)
+
+
+def json_iso_dttm_ser(obj):
+ """
+ json serializer that deals with dates
+
+ >>> dttm = datetime(1970, 1, 1)
+ >>> json.dumps({'dttm': dttm}, default=json_iso_dttm_ser)
+ '{"dttm": "1970-01-01T00:00:00"}'
+ """
+ if isinstance(obj, datetime):
+ obj = obj.isoformat()
+ return obj
+
+
+def markdown(s):
+ s = s or ''
+ return md(s, [
+ 'markdown.extensions.tables',
+ 'markdown.extensions.fenced_code',
+ 'markdown.extensions.codehilite',])
+
+
+def readfile(filepath):
+ with open(filepath) as f:
+ content = f.read()
+ return content
diff --git a/dashed/views.py b/dashed/views.py
new file mode 100644
index 000000000..9720dbdd4
--- /dev/null
+++ b/dashed/views.py
@@ -0,0 +1,774 @@
+"""Flask web views for Dashed"""
+
+from datetime import datetime
+import json
+import logging
+import re
+import traceback
+
+from flask import request, redirect, flash, Response, render_template, Markup
+from flask.ext.appbuilder import ModelView, CompactCRUDMixin, BaseView, expose
+from flask.ext.appbuilder.actions import action
+from flask.ext.appbuilder.models.sqla.interface import SQLAInterface
+from flask.ext.appbuilder.security.decorators import has_access
+from pydruid.client import doublesum
+from sqlalchemy import create_engine
+import sqlalchemy as sqla
+from wtforms.validators import ValidationError
+import pandas as pd
+from sqlalchemy import select, text
+from sqlalchemy.sql.expression import TextAsFrom
+
+from dashed import appbuilder, db, models, viz, utils, app, sm, ascii_art
+
+config = app.config
+log_this = models.Log.log_this
+
+
+def validate_json(form, field): # noqa
+ try:
+ json.loads(field.data)
+ except Exception as e:
+ logging.exception(e)
+ raise ValidationError("json isn't valid")
+
+
+class DeleteMixin(object):
+ @action(
+ "muldelete", "Delete", "Delete all Really?", "fa-trash", single=False)
+ def muldelete(self, items):
+ self.datamodel.delete_all(items)
+ self.update_redirect()
+ return redirect(self.get_redirect())
+
+
+class DashedModelView(ModelView):
+ page_size = 500
+
+
+class TableColumnInlineView(CompactCRUDMixin, DashedModelView): # noqa
+ datamodel = SQLAInterface(models.TableColumn)
+ can_delete = False
+ edit_columns = [
+ 'column_name', 'description', 'groupby', 'filterable', 'table',
+ 'count_distinct', 'sum', 'min', 'max', 'expression', 'is_dttm']
+ add_columns = edit_columns
+ list_columns = [
+ 'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
+ 'sum', 'min', 'max', 'is_dttm']
+ page_size = 500
+ description_columns = {
+ 'is_dttm': (
+ "Whether to make this column available as a "
+ "[Time Granularity] option, column has to be DATETIME or "
+ "DATETIME-like"),
+ }
+appbuilder.add_view_no_menu(TableColumnInlineView)
+
+appbuilder.add_link(
+ "Featured Datasets",
+ href='/dashed/featured',
+ category='Sources',
+ category_icon='fa-table',
+ icon="fa-star")
+
+appbuilder.add_separator("Sources")
+
+
+class DruidColumnInlineView(CompactCRUDMixin, DashedModelView): # noqa
+ datamodel = SQLAInterface(models.DruidColumn)
+ edit_columns = [
+ 'column_name', 'description', 'datasource', 'groupby',
+ 'count_distinct', 'sum', 'min', 'max']
+ list_columns = [
+ 'column_name', 'type', 'groupby', 'filterable', 'count_distinct',
+ 'sum', 'min', 'max']
+ can_delete = False
+ page_size = 500
+
+ def post_update(self, col):
+ col.generate_metrics()
+
+appbuilder.add_view_no_menu(DruidColumnInlineView)
+
+
+class SqlMetricInlineView(CompactCRUDMixin, DashedModelView): # noqa
+ datamodel = SQLAInterface(models.SqlMetric)
+ list_columns = ['metric_name', 'verbose_name', 'metric_type']
+ edit_columns = [
+ 'metric_name', 'description', 'verbose_name', 'metric_type',
+ 'expression', 'table']
+ add_columns = edit_columns
+ page_size = 500
+appbuilder.add_view_no_menu(SqlMetricInlineView)
+
+
+class DruidMetricInlineView(CompactCRUDMixin, DashedModelView): # noqa
+ datamodel = SQLAInterface(models.DruidMetric)
+ list_columns = ['metric_name', 'verbose_name', 'metric_type']
+ edit_columns = [
+ 'metric_name', 'description', 'verbose_name', 'metric_type',
+ 'datasource', 'json']
+ add_columns = [
+ 'metric_name', 'verbose_name', 'metric_type', 'datasource', 'json']
+ page_size = 500
+ validators_columns = {
+ 'json': [validate_json],
+ }
+appbuilder.add_view_no_menu(DruidMetricInlineView)
+
+
+class DatabaseView(DashedModelView, 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']
+ search_exclude_columns = ('password',)
+ edit_columns = add_columns
+ add_template = "dashed/models/database/add.html"
+ edit_template = "dashed/models/database/edit.html"
+ base_order = ('changed_on','desc')
+ description_columns = {
+ 'sqlalchemy_uri': (
+ "Refer to the SqlAlchemy docs for more information on how "
+ "to structure your URI here: "
+ "http://docs.sqlalchemy.org/en/rel_1_0/core/engines.html")
+ }
+ def pre_add(self, db):
+ conn = sqla.engine.url.make_url(db.sqlalchemy_uri)
+ db.password = conn.password
+ conn.password = "X" * 10 if conn.password else None
+ db.sqlalchemy_uri = str(conn) # hides the password
+
+ def pre_update(self, db):
+ self.pre_add(db)
+
+
+appbuilder.add_view(
+ DatabaseView,
+ "Databases",
+ icon="fa-database",
+ category="Sources",
+ category_icon='fa-database',)
+
+
+class TableModelView(DashedModelView, DeleteMixin): # noqa
+ datamodel = SQLAInterface(models.SqlaTable)
+ list_columns = [
+ 'table_link', 'database', 'sql_link', 'is_featured',
+ 'changed_by_', 'changed_on']
+ add_columns = ['table_name', 'database', 'default_endpoint', 'offset']
+ edit_columns = [
+ 'table_name', 'is_featured', 'database', 'description', 'owner',
+ 'main_dttm_col', 'default_endpoint', 'offset']
+ related_views = [TableColumnInlineView, SqlMetricInlineView]
+ base_order = ('changed_on','desc')
+ description_columns = {
+ 'offset': "Timezone offset (in hours) for this datasource",
+ 'description': Markup(
+ "Supports "
+ "markdown "),
+ }
+
+ def post_add(self, table):
+ try:
+ table.fetch_metadata()
+ except Exception as e:
+ logging.exception(e)
+ flash(
+ "Table [{}] doesn't seem to exist, "
+ "couldn't fetch metadata".format(table.table_name),
+ "danger")
+ utils.merge_perm(sm, 'datasource_access', table.perm)
+
+ def post_update(self, table):
+ self.post_add(table)
+
+appbuilder.add_view(
+ TableModelView,
+ "Tables",
+ category="Sources",
+ icon='fa-table',)
+
+
+appbuilder.add_separator("Sources")
+
+
+class DruidClusterModelView(DashedModelView, DeleteMixin): # noqa
+ datamodel = SQLAInterface(models.DruidCluster)
+ add_columns = [
+ 'cluster_name',
+ 'coordinator_host', 'coordinator_port', 'coordinator_endpoint',
+ 'broker_host', 'broker_port', 'broker_endpoint',
+ ]
+ edit_columns = add_columns
+ list_columns = ['cluster_name', 'metadata_last_refreshed']
+
+appbuilder.add_view(
+ DruidClusterModelView,
+ "Druid Clusters",
+ icon="fa-cubes",
+ category="Sources",
+ category_icon='fa-database',)
+
+
+class SliceModelView(DashedModelView, DeleteMixin): # noqa
+ datamodel = SQLAInterface(models.Slice)
+ can_add = False
+ list_columns = [
+ 'slice_link', 'viz_type',
+ 'datasource_link', 'created_by_', 'changed_on']
+ order_columns = utils.list_minus(list_columns, ['created_by_'])
+ edit_columns = [
+ 'slice_name', 'description', 'viz_type', 'druid_datasource',
+ 'table', 'dashboards', 'params']
+ base_order = ('changed_on','desc')
+ description_columns = {
+ 'description': Markup(
+ "The content here can be displayed as widget headers in the "
+ "dashboard view. Supports "
+ ""
+ "markdown "),
+ }
+
+
+appbuilder.add_view(
+ SliceModelView,
+ "Slices",
+ icon="fa-bar-chart",
+ category="",
+ category_icon='',)
+
+
+class DashboardModelView(DashedModelView, DeleteMixin): # noqa
+ datamodel = SQLAInterface(models.Dashboard)
+ list_columns = ['dashboard_link', 'created_by_', 'changed_on']
+ order_columns = utils.list_minus(list_columns, ['created_by_'])
+ edit_columns = [
+ 'dashboard_title', 'slug', 'slices', 'position_json', 'css',
+ 'json_metadata']
+ add_columns = edit_columns
+ base_order = ('changed_on','desc')
+ description_columns = {
+ 'position_json': (
+ "This json object describes the positioning of the widgets in "
+ "the dashboard. It is dynamically generated when adjusting "
+ "the widgets size and positions by using drag & drop in "
+ "the dashboard view"),
+ 'css': (
+ "The css for individual dashboards can be altered here, or "
+ "in the dashboard view where changes are immediately "
+ "visible"),
+ 'slug': "To get a readable URL for your dashboard",
+ }
+ def pre_add(self, obj):
+ obj.slug = obj.slug.strip() or None
+ if obj.slug:
+ obj.slug = obj.slug.replace(" ", "-")
+ obj.slug = re.sub(r'\W+', '', obj.slug)
+
+ def pre_update(self, obj):
+ self.pre_add(obj)
+
+
+appbuilder.add_view(
+ DashboardModelView,
+ "Dashboards",
+ icon="fa-dashboard",
+ category="",
+ category_icon='',)
+
+
+class LogModelView(DashedModelView):
+ datamodel = SQLAInterface(models.Log)
+ list_columns = ('user', 'action', 'dttm')
+ edit_columns = ('user', 'action', 'dttm', 'json')
+ base_order = ('dttm','desc')
+
+appbuilder.add_view(
+ LogModelView,
+ "Action Log",
+ category="Security",
+ icon="fa-list-ol")
+
+
+class DruidDatasourceModelView(DashedModelView, DeleteMixin): # noqa
+ datamodel = SQLAInterface(models.DruidDatasource)
+ list_columns = [
+ 'datasource_link', 'cluster', 'owner',
+ 'created_by_', 'created_on',
+ 'changed_by_', 'changed_on',
+ 'offset']
+ order_columns = utils.list_minus(
+ list_columns, ['created_by_', 'changed_by_'])
+ related_views = [DruidColumnInlineView, DruidMetricInlineView]
+ edit_columns = [
+ 'datasource_name', 'cluster', 'description', 'owner',
+ 'is_featured', 'is_hidden', 'default_endpoint', 'offset']
+ page_size = 500
+ base_order = ('datasource_name', 'asc')
+ description_columns = {
+ 'offset': "Timezone offset (in hours) for this datasource",
+ 'description': Markup(
+ "Supports markdown "),
+ }
+
+ def post_add(self, datasource):
+ datasource.generate_metrics()
+ utils.merge_perm(sm, 'datasource_access', datasource.perm)
+
+ def post_update(self, datasource):
+ self.post_add(datasource)
+
+appbuilder.add_view(
+ DruidDatasourceModelView,
+ "Druid Datasources",
+ category="Sources",
+ icon="fa-cube")
+
+
+@app.route('/health')
+def health():
+ return "OK"
+
+
+@app.route('/ping')
+def ping():
+ return "OK"
+
+
+class R(BaseView):
+
+ @log_this
+ @expose("/")
+ def index(self, url_id):
+ url = db.session.query(models.Url).filter_by(id=url_id).first()
+ if url:
+ print(url.url)
+ return redirect('/' + url.url)
+ else:
+ flash("URL to nowhere...", "danger")
+ return redirect('/')
+
+ @log_this
+ @expose("/shortner/", methods=['POST', 'GET'])
+ def shortner(self):
+ url = request.form.get('data')
+ obj = models.Url(url=url)
+ db.session.add(obj)
+ db.session.commit()
+ return("{request.headers[Host]}/r/{obj.id}".format(
+ request=request, obj=obj))
+
+appbuilder.add_view_no_menu(R)
+
+
+class Dashed(BaseView):
+
+ """The base views for Dashed!"""
+
+ @has_access
+ @expose("/explore///")
+ @expose("/datasource///") # Legacy url
+ @log_this
+ def explore(self, datasource_type, datasource_id):
+ if datasource_type == "table":
+ datasource = (
+ db.session
+ .query(models.SqlaTable)
+ .filter_by(id=datasource_id)
+ .first()
+ )
+ else:
+ datasource = (
+ db.session
+ .query(models.DruidDatasource)
+ .filter_by(id=datasource_id)
+ .first()
+ )
+
+ all_datasource_access = self.appbuilder.sm.has_access(
+ 'all_datasource_access', 'all_datasource_access')
+ datasource_access = self.appbuilder.sm.has_access(
+ 'datasource_access', datasource.perm)
+ if not (all_datasource_access or datasource_access):
+ flash(
+ "You don't seem to have access to this datasource",
+ "danger")
+ return redirect('/slicemodelview/list/')
+ action = request.args.get('action')
+ if action in ('save', 'overwrite'):
+ session = db.session()
+
+ # TODO use form processing form wtforms
+ d = request.args.to_dict(flat=False)
+ del d['action']
+ del d['previous_viz_type']
+ as_list = ('metrics', 'groupby', 'columns')
+ for k in d:
+ v = d.get(k)
+ if k in as_list and not isinstance(v, list):
+ d[k] = [v] if v else []
+ if k not in as_list and isinstance(v, list):
+ d[k] = v[0]
+
+ table_id = druid_datasource_id = None
+ datasource_type = request.args.get('datasource_type')
+ if datasource_type in ('datasource', 'druid'):
+ druid_datasource_id = request.args.get('datasource_id')
+ elif datasource_type == 'table':
+ table_id = request.args.get('datasource_id')
+
+ slice_name = request.args.get('slice_name')
+
+ if action == "save":
+ slc = models.Slice()
+ msg = "Slice [{}] has been saved".format(slice_name)
+ elif action == "overwrite":
+ slc = (
+ session.query(models.Slice)
+ .filter_by(id=request.args.get("slice_id"))
+ .first()
+ )
+ msg = "Slice [{}] has been overwritten".format(slice_name)
+
+ slc.params = json.dumps(d, indent=4, sort_keys=True)
+ slc.datasource_name = request.args.get('datasource_name')
+ slc.viz_type = request.args.get('viz_type')
+ slc.druid_datasource_id = druid_datasource_id
+ slc.table_id = table_id
+ slc.datasource_type = datasource_type
+ slc.slice_name = slice_name
+
+ session.merge(slc)
+ session.commit()
+ flash(msg, "info")
+ return redirect(slc.slice_url)
+
+
+ if not datasource:
+ flash("The datasource seem to have been deleted", "alert")
+ viz_type = request.args.get("viz_type")
+ if not viz_type and datasource.default_endpoint:
+ return redirect(datasource.default_endpoint)
+ if not viz_type:
+ viz_type = "table"
+ obj = viz.viz_types[viz_type](
+ datasource,
+ form_data=request.args)
+ if request.args.get("csv") == "true":
+ status = 200
+ payload = obj.get_csv()
+ return Response(
+ payload,
+ status=status,
+ mimetype="application/csv")
+
+ slice_id = request.args.get("slice_id")
+ slc = None
+ if slice_id:
+ slc = (
+ db.session.query(models.Slice)
+ .filter_by(id=request.args.get("slice_id"))
+ .first()
+ )
+ if request.args.get("json") == "true":
+ status = 200
+ if config.get("DEBUG"):
+ payload = obj.get_json()
+ else:
+ try:
+ payload = obj.get_json()
+ except Exception as e:
+ logging.exception(e)
+ payload = str(e)
+ status = 500
+ return Response(
+ payload,
+ status=status,
+ mimetype="application/json")
+ else:
+ if config.get("DEBUG"):
+ resp = self.render_template(
+ "dashed/viz.html", viz=obj, slice=slc)
+ try:
+ resp = self.render_template(
+ "dashed/viz.html", viz=obj, slice=slc)
+ except Exception as e:
+ if config.get("DEBUG"):
+ raise(e)
+ return Response(
+ str(e),
+ status=500,
+ mimetype="application/json")
+ return resp
+
+ @has_access
+ @expose("/checkbox////", methods=['GET'])
+ def checkbox(self, model_view, id_, attr, value):
+ """endpoint for checking/unchecking any boolean in a sqla model"""
+ model = None
+ if model_view == 'TableColumnInlineView':
+ model = models.TableColumn
+ elif model_view == 'DruidColumnInlineView':
+ model = models.DruidColumn
+
+ obj = db.session.query(model).filter_by(id=id_).first()
+ if obj:
+ setattr(obj, attr, value=='true')
+ db.session.commit()
+ return Response("OK", mimetype="application/json")
+
+
+ @has_access
+ @expose("/save_dash//", methods=['GET', 'POST'])
+ def save_dash(self, dashboard_id):
+ """Save a dashboard's metadata"""
+ data = json.loads(request.form.get('data'))
+ positions = data['positions']
+ slice_ids = [int(d['slice_id']) for d in positions]
+ session = db.session()
+ Dash = models.Dashboard
+ dash = session.query(Dash).filter_by(id=dashboard_id).first()
+ dash.slices = [o for o in dash.slices if o.id in slice_ids]
+ dash.position_json = json.dumps(data['positions'], indent=4)
+ md = dash.metadata_dejson
+ if 'filter_immune_slices' not in md:
+ md['filter_immune_slices'] = []
+ md['expanded_slices'] = data['expanded_slices']
+ dash.json_metadata = json.dumps(md, indent=4)
+ dash.css = data['css']
+ session.merge(dash)
+ session.commit()
+ session.close()
+ return "SUCCESS"
+
+ @has_access
+ @expose("/testconn", methods=["POST", "GET"])
+ def testconn(self):
+ """Tests a sqla connection"""
+ try:
+ uri = request.form.get('uri')
+ engine = create_engine(uri)
+ engine.connect()
+ return json.dumps(engine.table_names(), indent=4)
+ except Exception:
+ return Response(
+ traceback.format_exc(),
+ status=500,
+ mimetype="application/json")
+
+ @has_access
+ @expose("/dashboard//")
+ def dashboard(self, dashboard_id):
+ """Server side rendering for a dashboard"""
+ session = db.session()
+ qry = session.query(models.Dashboard)
+ if dashboard_id.isdigit():
+ qry = qry.filter_by(id=int(dashboard_id))
+ else:
+ qry = qry.filter_by(slug=dashboard_id)
+
+ templates = session.query(models.CssTemplate).all()
+
+ dash = qry.first()
+
+ # Hack to log the dashboard_id properly, even when getting a slug
+ @log_this
+ def dashboard(**kwargs): # noqa
+ pass
+ dashboard(dashboard_id=dash.id)
+
+ pos_dict = {}
+ if dash.position_json:
+ pos_dict = {
+ int(o['slice_id']):o
+ for o in json.loads(dash.position_json)}
+ return self.render_template(
+ "dashed/dashboard.html", dashboard=dash,
+ templates=templates,
+ pos_dict=pos_dict)
+
+ @has_access
+ @expose("/sql//")
+ @log_this
+ def sql(self, database_id):
+ mydb = db.session.query(
+ models.Database).filter_by(id=database_id).first()
+ engine = mydb.get_sqla_engine()
+ tables = engine.table_names()
+
+ table_name=request.args.get('table_name')
+ return self.render_template(
+ "dashed/sql.html",
+ tables=tables,
+ table_name=table_name,
+ db=mydb)
+
+ @has_access
+ @expose("/table///")
+ @log_this
+ def table(self, database_id, table_name):
+ mydb = db.session.query(
+ models.Database).filter_by(id=database_id).first()
+ cols = mydb.get_columns(table_name)
+ df = pd.DataFrame([(c['name'], c['type']) for c in cols])
+ df.columns = ['col', 'type']
+ return self.render_template(
+ "dashed/ajah.html",
+ content=df.to_html(
+ index=False,
+ na_rep='',
+ classes=(
+ "dataframe table table-striped table-bordered "
+ "table-condensed sql_results")))
+
+ @has_access
+ @expose("/select_star///")
+ @log_this
+ def select_star(self, database_id, table_name):
+ mydb = db.session.query(
+ models.Database).filter_by(id=database_id).first()
+ t = mydb.get_table(table_name)
+ fields = ", ".join(
+ [c.name for c in t.columns] or "*")
+ s = "SELECT\n{}\nFROM {}".format(fields, table_name)
+ return self.render_template(
+ "dashed/ajah.html",
+ content=s
+ )
+
+ @has_access
+ @expose("/runsql/", methods=['POST', 'GET'])
+ @log_this
+ def runsql(self):
+ session = db.session()
+ limit = 1000
+ data = json.loads(request.form.get('data'))
+ sql = data.get('sql')
+ database_id = data.get('database_id')
+ mydb = session.query(models.Database).filter_by(id=database_id).first()
+ content = ""
+ if mydb:
+ eng = mydb.get_sqla_engine()
+ if limit:
+ sql = sql.strip().strip(';')
+ qry = (
+ select('*')
+ .select_from(TextAsFrom(text(sql), ['*']).alias('inner_qry'))
+ .limit(limit)
+ )
+ sql= str(qry.compile(eng, compile_kwargs={"literal_binds": True}))
+ try:
+ df = pd.read_sql_query(sql=sql, con=eng)
+ content = df.to_html(
+ index=False,
+ na_rep='',
+ classes=(
+ "dataframe table table-striped table-bordered "
+ "table-condensed sql_results"))
+ except Exception as e:
+ content = (
+ ''
+ "{}
"
+ ).format(e.message)
+ session.commit()
+ return content
+
+ @has_access
+ @expose("/refresh_datasources/")
+ def refresh_datasources(self):
+ session = db.session()
+ for cluster in session.query(models.DruidCluster).all():
+ try:
+ cluster.refresh_datasources()
+ except Exception as e:
+ flash(
+ "Error while processing cluster '{}'\n{}".format(
+ cluster, str(e)),
+ "danger")
+ logging.exception(e)
+ return redirect('/druidclustermodelview/list/')
+ cluster.metadata_last_refreshed = datetime.now()
+ flash(
+ "Refreshed metadata from cluster "
+ "[" + cluster.cluster_name + "]",
+ 'info')
+ session.commit()
+ return redirect("/datasourcemodelview/list/")
+
+ @expose("/autocomplete///")
+ def autocomplete(self, datasource, column):
+ client = utils.get_pydruid_client()
+ top = client.topn(
+ datasource=datasource,
+ granularity='all',
+ intervals='2013-10-04/2020-10-10',
+ aggregations={"count": doublesum("count")},
+ dimension=column,
+ metric='count',
+ threshold=1000,
+ )
+ values = sorted([d[column] for d in top[0]['result']])
+ return json.dumps(values)
+
+ @app.errorhandler(500)
+ def show_traceback(self):
+ if config.get("SHOW_STACKTRACE"):
+ error_msg = traceback.format_exc()
+ else:
+ error_msg = "FATAL ERROR\n"
+ error_msg = (
+ "Stacktrace is hidden. Change the SHOW_STACKTRACE "
+ "configuration setting to enable it")
+ return render_template(
+ 'dashed/traceback.html',
+ error_msg=error_msg,
+ title=ascii_art.stacktrace,
+ art=ascii_art.error), 500
+
+ @has_access
+ @expose("/featured", methods=['GET'])
+ def featured(self):
+ session = db.session()
+ datasets_sqla = (
+ session.query(models.SqlaTable)
+ .filter_by(is_featured=True)
+ .all()
+ )
+ datasets_druid = (
+ session.query(models.DruidDatasource)
+ .filter_by(is_featured=True)
+ .all()
+ )
+ featured_datasets = datasets_sqla + datasets_druid
+ return self.render_template(
+ 'dashed/featured.html',
+ featured_datasets=featured_datasets,
+ utils=utils)
+
+appbuilder.add_view_no_menu(Dashed)
+appbuilder.add_link(
+ "Refresh Druid Metadata",
+ href='/dashed/refresh_datasources/',
+ category='Sources',
+ category_icon='fa-database',
+ icon="fa-cog")
+
+
+class CssTemplateModelView(DashedModelView, DeleteMixin):
+ datamodel = SQLAInterface(models.CssTemplate)
+ list_columns = ['template_name']
+ edit_columns = ['template_name', 'css']
+ add_columns = edit_columns
+
+appbuilder.add_separator("Sources")
+appbuilder.add_view(
+ CssTemplateModelView,
+ "CSS Templates",
+ icon="fa-css3",
+ category="Sources",
+ category_icon='',)
+
+
diff --git a/dashed/viz.py b/dashed/viz.py
new file mode 100644
index 000000000..56b15a70b
--- /dev/null
+++ b/dashed/viz.py
@@ -0,0 +1,1228 @@
+"""
+This module contains the "Viz" objects that represent the backend of all
+the visualizations that Dashed can render
+"""
+
+from collections import OrderedDict, defaultdict
+from datetime import datetime, timedelta
+import json
+import uuid
+
+from flask import flash, request, Markup
+from markdown import markdown
+from pandas.io.json import dumps
+from werkzeug.datastructures import ImmutableMultiDict
+from werkzeug.urls import Href
+import pandas as pd
+
+from dashed import app, utils
+from dashed.forms import FormFactory
+
+from six import string_types
+
+config = app.config
+
+
+class BaseViz(object):
+
+ """All visualizations derive this base class"""
+
+ viz_type = None
+ verbose_name = "Base Viz"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'metrics', 'groupby',
+ )
+ },)
+ form_overrides = {}
+
+ def __init__(self, datasource, form_data):
+ self.orig_form_data = form_data
+ self.datasource = datasource
+ self.request = request
+ self.viz_type = form_data.get("viz_type")
+
+ # TODO refactor all form related logic out of here and into forms.py
+ ff = FormFactory(self)
+ form_class = ff.get_form()
+ defaults = form_class().data.copy()
+ previous_viz_type = form_data.get('previous_viz_type')
+ if isinstance(form_data, ImmutableMultiDict):
+ form = form_class(form_data)
+ else:
+ form = form_class(**form_data)
+ data = form.data.copy()
+
+ if not form.validate():
+ for k, v in form.errors.items():
+ if not data.get('json') and not data.get('async'):
+ flash("{}: {}".format(k, " ".join(v)), 'danger')
+ if previous_viz_type != self.viz_type:
+ data = {
+ k: form.data[k]
+ for k in form_data.keys()
+ if k in form.data}
+ defaults.update(data)
+ self.form_data = defaults
+ self.query = ""
+
+ self.form_data['previous_viz_type'] = self.viz_type
+ self.token = self.form_data.get(
+ 'token', 'token_' + uuid.uuid4().hex[:8])
+
+ self.metrics = self.form_data.get('metrics') or []
+ self.groupby = self.form_data.get('groupby') or []
+ self.reassignments()
+
+ def get_form_override(self, fieldname, attr):
+ if (
+ fieldname in self.form_overrides and
+ attr in self.form_overrides[fieldname]):
+ s = self.form_overrides[fieldname][attr]
+ if attr == 'label':
+ s = '{s} '.format(**locals())
+ s = Markup(s)
+ return s
+
+ @classmethod
+ def flat_form_fields(cls):
+ l = set()
+ for d in cls.fieldsets:
+ for obj in d['fields']:
+ if obj and isinstance(obj, (tuple, list)):
+ l |= {a for a in obj if a}
+ elif obj:
+ l.add(obj)
+ return tuple(l)
+
+ def reassignments(self):
+ pass
+
+ def get_url(self, **kwargs):
+ d = self.orig_form_data.copy()
+ if 'json' in d:
+ del d['json']
+ if 'action' in d:
+ del d['action']
+ d.update(kwargs)
+ # Remove unchecked checkboxes because HTML is weird like that
+ for key in d.keys():
+ if d[key] is False:
+ del d[key]
+ href = Href(
+ '/dashed/explore/{self.datasource.type}/'
+ '{self.datasource.id}/'.format(**locals()))
+ return href(d)
+
+ def get_df(self, query_obj=None):
+ """Returns a pandas dataframe based on the query object"""
+ if not query_obj:
+ query_obj = self.query_obj()
+
+ self.error_msg = ""
+ self.results = None
+
+ # The datasource here can be different backend but the interface is common
+ self.results = self.datasource.query(**query_obj)
+ self.query = self.results.query
+ df = self.results.df
+ if df is None or df.empty:
+ raise Exception("No data, review your incantations!")
+ else:
+ if 'timestamp' in df.columns:
+ df.timestamp = pd.to_datetime(df.timestamp, utc=False)
+ if self.datasource.offset:
+ df.timestamp += timedelta(hours=self.datasource.offset)
+ df = df.fillna(0)
+ return df
+
+ @property
+ def form(self):
+ return self.form_class(**self.form_data)
+
+ @property
+ def form_class(self):
+ return FormFactory(self).get_form()
+
+ def query_filters(self):
+ """Processes the filters for the query"""
+ form_data = self.form_data
+ # Building filters
+ filters = []
+ for i in range(1, 10):
+ col = form_data.get("flt_col_" + str(i))
+ op = form_data.get("flt_op_" + str(i))
+ eq = form_data.get("flt_eq_" + str(i))
+ if col and op and eq:
+ filters.append((col, op, eq))
+
+ # Extra filters (coming from dashboard)
+ extra_filters = form_data.get('extra_filters')
+ if extra_filters:
+ extra_filters = json.loads(extra_filters)
+ for slice_filters in extra_filters.values():
+ for col, vals in slice_filters.items():
+ if col and vals:
+ filters += [(col, 'in', ",".join(vals))]
+ return filters
+
+ def query_obj(self):
+ """Building a query object"""
+ form_data = self.form_data
+ groupby = form_data.get("groupby") or []
+ metrics = form_data.get("metrics") or ['count']
+ granularity = \
+ form_data.get("granularity") or form_data.get("granularity_sqla")
+ limit = int(form_data.get("limit", 0))
+ row_limit = int(
+ form_data.get("row_limit", config.get("ROW_LIMIT")))
+ since = form_data.get("since", "1 year ago")
+ from_dttm = utils.parse_human_datetime(since)
+ if from_dttm > datetime.now():
+ from_dttm = datetime.now() - (from_dttm-datetime.now())
+ until = form_data.get("until", "now")
+ to_dttm = utils.parse_human_datetime(until)
+ if from_dttm > to_dttm:
+ flash("The date range doesn't seem right.", "danger")
+ from_dttm = to_dttm # Making them identical to not raise
+
+ # extras are used to query elements specific to a datasource type
+ # for instance the extra where clause that applies only to Tables
+ extras = {
+ 'where': form_data.get("where", ''),
+ 'having': form_data.get("having", ''),
+ 'time_grain_sqla': form_data.get("time_grain_sqla", ''),
+ }
+ d = {
+ 'granularity': granularity,
+ 'from_dttm': from_dttm,
+ 'to_dttm': to_dttm,
+ 'is_timeseries': self.is_timeseries,
+ 'groupby': groupby,
+ 'metrics': metrics,
+ 'row_limit': row_limit,
+ 'filter': self.query_filters(),
+ 'timeseries_limit': limit,
+ 'extras': extras,
+ }
+ return d
+
+ def get_json(self):
+ payload = {
+ 'data': json.loads(self.get_json_data()),
+ 'query': self.query,
+ 'form_data': self.form_data,
+ 'json_endpoint': self.json_endpoint,
+ 'csv_endpoint': self.csv_endpoint,
+ 'standalone_endpoint': self.standalone_endpoint,
+ }
+ return json.dumps(payload)
+
+ def get_csv(self):
+ df = self.get_df()
+ return df.to_csv(index=False)
+
+ def get_json_data(self):
+ return json.dumps([])
+
+ @property
+ def json_endpoint(self):
+ return self.get_url(json="true")
+
+ @property
+ def csv_endpoint(self):
+ return self.get_url(csv="true")
+
+ @property
+ def standalone_endpoint(self):
+ return self.get_url(standalone="true")
+
+ @property
+ def data(self):
+ content = {
+ 'viz_name': self.viz_type,
+ 'json_endpoint': self.json_endpoint,
+ 'csv_endpoint': self.csv_endpoint,
+ 'standalone_endpoint': self.standalone_endpoint,
+ 'token': self.token,
+ 'form_data': self.form_data,
+ }
+ return content
+
+ @property
+ def json_data(self):
+ return dumps(self.data)
+
+class TableViz(BaseViz):
+ viz_type = "table"
+ verbose_name = "Table View"
+ fieldsets = (
+ {
+ 'label': "Chart Options",
+ 'fields': (
+ 'row_limit',
+ ('include_search', None),
+ )
+ },
+ {
+ 'label': "GROUP BY",
+ 'fields': (
+ 'groupby',
+ 'metrics',
+ )
+ },
+ {
+ 'label': "NOT GROUPED BY",
+ 'fields': (
+ 'all_columns',
+ )
+ },)
+ is_timeseries = False
+
+
+ def query_obj(self):
+ d = super(TableViz, self).query_obj()
+ fd = self.form_data
+ if fd.get('all_columns') and (fd.get('groupby') or fd.get('metrics')):
+ raise Exception(
+ "Choose either fields to [Group By] and [Metrics] or "
+ "[Columns], not both")
+ if fd.get('all_columns'):
+ d['columns'] = fd.get('all_columns')
+ d['groupby'] = []
+ return d
+
+ def get_df(self, query_obj=None):
+ df = super(TableViz, self).get_df(query_obj)
+ if (
+ self.form_data.get("granularity") == "all" and
+ 'timestamp' in df):
+ del df['timestamp']
+ return df
+
+ def get_json_data(self):
+ df = self.get_df()
+ return json.dumps(
+ dict(
+ records=df.to_dict(orient="records"),
+ columns=list(df.columns),
+ ),
+ default=utils.json_iso_dttm_ser,
+ )
+
+
+class PivotTableViz(BaseViz):
+ viz_type = "pivot_table"
+ verbose_name = "Pivot Table"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'groupby',
+ 'columns',
+ 'metrics',
+ 'pandas_aggfunc',
+ )
+ },)
+
+ def query_obj(self):
+ d = super(PivotTableViz, self).query_obj()
+ groupby = self.form_data.get('groupby')
+ columns = self.form_data.get('columns')
+ metrics = self.form_data.get('metrics')
+ if not columns:
+ columns = []
+ if not groupby:
+ groupby = []
+ if not groupby:
+ raise Exception("Please choose at least one \"Group by\" field ")
+ if not metrics:
+ raise Exception("Please choose at least one metric")
+ if (
+ any(v in groupby for v in columns) or
+ any(v in columns for v in groupby)):
+ raise Exception("groupby and columns can't overlap")
+
+ d['groupby'] = list(set(groupby) | set(columns))
+ return d
+
+ def get_df(self, query_obj=None):
+ df = super(PivotTableViz, self).get_df(query_obj)
+ if (
+ self.form_data.get("granularity") == "all" and
+ 'timestamp' in df):
+ del df['timestamp']
+ df = df.pivot_table(
+ index=self.form_data.get('groupby'),
+ columns=self.form_data.get('columns'),
+ values=self.form_data.get('metrics'),
+ aggfunc=self.form_data.get('pandas_aggfunc'),
+ margins=True,
+ )
+ return df
+
+ def get_json_data(self):
+ return dumps(self.get_df().to_html(
+ na_rep='',
+ classes=(
+ "dataframe table table-striped table-bordered "
+ "table-condensed table-hover")))
+
+
+class MarkupViz(BaseViz):
+ viz_type = "markup"
+ verbose_name = "Markup Widget"
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': ('markup_type', 'code')
+ },)
+ is_timeseries = False
+
+ def rendered(self):
+ markup_type = self.form_data.get("markup_type")
+ code = self.form_data.get("code", '')
+ if markup_type == "markdown":
+ return markdown(code)
+ elif markup_type == "html":
+ return code
+
+ def get_json_data(self):
+ return dumps(dict(html=self.rendered()))
+
+
+class WordCloudViz(BaseViz):
+
+ """Integration with the nice library at:
+
+ https://github.com/jasondavies/d3-cloud
+ """
+
+ viz_type = "word_cloud"
+ verbose_name = "Word Cloud"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'series', 'metric', 'limit',
+ ('size_from', 'size_to'),
+ 'rotation',
+ )
+ },)
+
+ def query_obj(self):
+ d = super(WordCloudViz, self).query_obj()
+
+ d['metrics'] = [self.form_data.get('metric')]
+ d['groupby'] = [self.form_data.get('series')]
+ return d
+
+ def get_json_data(self):
+ df = self.get_df()
+ # Ordering the columns
+ df = df[[self.form_data.get('series'), self.form_data.get('metric')]]
+ # Labeling the columns for uniform json schema
+ df.columns = ['text', 'size']
+ return df.to_json(orient="records")
+
+
+class NVD3Viz(BaseViz):
+
+ """Base class for all nvd3 vizs"""
+
+ viz_type = None
+ verbose_name = "Base NVD3 Viz"
+ is_timeseries = False
+
+
+class BubbleViz(NVD3Viz):
+
+ """Based on the NVD3 bubble chart"""
+
+ viz_type = "bubble"
+ verbose_name = "Bubble Chart"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'series', 'entity',
+ 'x', 'y',
+ 'size', 'limit',
+ )
+ },
+ {
+ 'label': 'Chart Options',
+ 'fields': (
+ ('x_log_scale', 'y_log_scale'),
+ ('show_legend', None),
+ 'max_bubble_size',
+ )
+ },)
+
+ def query_obj(self):
+ form_data = self.form_data
+ d = super(BubbleViz, self).query_obj()
+ d['groupby'] = list({
+ form_data.get('series'),
+ form_data.get('entity')
+ })
+ self.x_metric = form_data.get('x')
+ self.y_metric = form_data.get('y')
+ self.z_metric = form_data.get('size')
+ self.entity = form_data.get('entity')
+ self.series = form_data.get('series')
+
+ d['metrics'] = [
+ self.z_metric,
+ self.x_metric,
+ self.y_metric,
+ ]
+ if not all(d['metrics'] + [self.entity, self.series]):
+ raise Exception("Pick a metric for x, y and size")
+ return d
+
+ def get_df(self, query_obj=None):
+ df = super(BubbleViz, self).get_df(query_obj)
+ df = df.fillna(0)
+ df['x'] = df[[self.x_metric]]
+ df['y'] = df[[self.y_metric]]
+ df['size'] = df[[self.z_metric]]
+ df['shape'] = 'circle'
+ df['group'] = df[[self.series]]
+ return df
+
+ def get_json_data(self):
+ df = self.get_df()
+ series = defaultdict(list)
+ for row in df.to_dict(orient='records'):
+ series[row['group']].append(row)
+ chart_data = []
+ for k, v in series.items():
+ chart_data.append({
+ 'key': k,
+ 'values': v })
+ return dumps(chart_data)
+
+class BigNumberViz(BaseViz):
+ viz_type = "big_number"
+ verbose_name = "Big Number"
+ is_timeseries = True
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'metric',
+ 'compare_lag',
+ 'compare_suffix',
+ '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(BigNumberViz, 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_json_data(self):
+ form_data = self.form_data
+ df = self.get_df()
+ df = df.sort(columns=df.columns[0])
+ compare_lag = form_data.get("compare_lag", "")
+ compare_lag = int(compare_lag) if compare_lag and compare_lag.isdigit() else 0
+ d = {
+ 'data': df.values.tolist(),
+ 'compare_lag': compare_lag,
+ 'compare_suffix': form_data.get('compare_suffix', ''),
+ }
+ return dumps(d)
+
+
+class NVD3TimeSeriesViz(NVD3Viz):
+ viz_type = "line"
+ verbose_name = "Time Series - Line Chart"
+ sort_series = False
+ is_timeseries = True
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'metrics',
+ 'groupby', 'limit',
+ ),
+ }, {
+ 'label': 'Chart Options',
+ 'fields': (
+ ('show_brush', 'show_legend'),
+ ('rich_tooltip', 'y_axis_zero'),
+ ('y_log_scale', 'contribution'),
+ ('x_axis_format', 'y_axis_format'),
+ ('line_interpolation', 'x_axis_showminmax'),
+ ),
+ }, {
+ 'label': 'Advanced Analytics',
+ 'description': (
+ "This section contains options "
+ "that allow for advanced analytical post processing "
+ "of query results"),
+ 'fields': (
+ ('rolling_type', 'rolling_periods'),
+ 'time_compare',
+ 'num_period_compare',
+ None,
+ ('resample_how', 'resample_rule',), 'resample_fillmethod'
+ ),
+ },
+ )
+
+ def get_df(self, query_obj=None):
+ form_data = self.form_data
+ df = super(NVD3TimeSeriesViz, self).get_df(query_obj)
+
+ df = df.fillna(0)
+ if form_data.get("granularity") == "all":
+ raise Exception("Pick a time granularity for your time series")
+
+ df = df.pivot_table(
+ index="timestamp",
+ columns=form_data.get('groupby'),
+ values=form_data.get('metrics'))
+
+ fm = form_data.get("resample_fillmethod")
+ if not fm:
+ fm = None
+ how = form_data.get("resample_how")
+ rule = form_data.get("resample_rule")
+ if how and rule:
+ df = df.resample(rule, how=how, fill_method=fm)
+ if not fm:
+ df = df.fillna(0)
+
+
+ if self.sort_series:
+ dfs = df.sum()
+ dfs.sort(ascending=False)
+ df = df[dfs.index]
+
+ if form_data.get("contribution"):
+ dft = df.T
+ df = (dft / dft.sum()).T
+
+ num_period_compare = form_data.get("num_period_compare")
+ if num_period_compare:
+ num_period_compare = int(num_period_compare)
+ df = (df / df.shift(num_period_compare)) - 1
+ df = df[num_period_compare:]
+
+ rolling_periods = form_data.get("rolling_periods")
+ rolling_type = form_data.get("rolling_type")
+
+ if rolling_type in ('mean', 'std', 'sum') and rolling_periods:
+ if rolling_type == 'mean':
+ df = pd.rolling_mean(df, int(rolling_periods), min_periods=0)
+ elif rolling_type == 'std':
+ df = pd.rolling_std(df, int(rolling_periods), min_periods=0)
+ elif rolling_type == 'sum':
+ df = pd.rolling_sum(df, int(rolling_periods), min_periods=0)
+ elif rolling_type == 'cumsum':
+ df = df.cumsum()
+ return df
+
+ def to_series(self, df, classed='', title_suffix=''):
+ series = df.to_dict('series')
+
+ chart_data = []
+ for name in df.T.index.tolist():
+ ys = series[name]
+ if df[name].dtype.kind not in "biufc":
+ continue
+ df['timestamp'] = pd.to_datetime(df.index, utc=False)
+ if isinstance(name, string_types):
+ series_title = name
+ else:
+ name = ["{}".format(s) for s in name]
+ if len(self.form_data.get('metrics')) > 1:
+ series_title = ", ".join(name)
+ else:
+ series_title = ", ".join(name[1:])
+ if title_suffix:
+ series_title += title_suffix
+
+ d = {
+ "key": series_title,
+ "classed": classed,
+ "values": [{'x': ds, 'y': ys[ds]} for ds in df.timestamp],
+ }
+ chart_data.append(d)
+ return chart_data
+
+ def get_json_data(self):
+ df = self.get_df()
+ chart_data = self.to_series(df)
+
+ time_compare = self.form_data.get('time_compare')
+ if time_compare:
+ query_object = self.query_obj()
+ delta = utils.parse_human_timedelta(time_compare)
+ query_object['inner_from_dttm'] = query_object['from_dttm']
+ query_object['inner_to_dttm'] = query_object['to_dttm']
+ query_object['from_dttm'] -= delta
+ query_object['to_dttm'] -= delta
+
+ df2 = self.get_df(query_object)
+ df2.index += delta
+ chart_data += self.to_series(
+ df2, classed='dashed', title_suffix="---")
+ chart_data = sorted(chart_data, key=lambda x: x['key'])
+ return dumps(chart_data)
+
+
+class NVD3TimeSeriesBarViz(NVD3TimeSeriesViz):
+ viz_type = "bar"
+ sort_series = True
+ verbose_name = "Time Series - Bar Chart"
+ fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{
+ 'label': 'Chart Options',
+ 'fields': (
+ ('show_brush', 'show_legend'),
+ ('rich_tooltip', 'y_axis_zero'),
+ ('y_log_scale', 'contribution'),
+ ('x_axis_format', 'y_axis_format'),
+ ('line_interpolation', 'bar_stacked'),
+ ('x_axis_showminmax', None),
+ ), }] + [NVD3TimeSeriesViz.fieldsets[2]]
+
+
+class NVD3CompareTimeSeriesViz(NVD3TimeSeriesViz):
+ viz_type = 'compare'
+ verbose_name = "Time Series - Percent Change"
+
+
+class NVD3TimeSeriesStackedViz(NVD3TimeSeriesViz):
+ viz_type = "area"
+ verbose_name = "Time Series - Stacked"
+ sort_series = True
+ fieldsets = [NVD3TimeSeriesViz.fieldsets[0]] + [{
+ 'label': 'Chart Options',
+ 'fields': (
+ ('show_brush', 'show_legend'),
+ ('rich_tooltip', 'y_axis_zero'),
+ ('y_log_scale', 'contribution'),
+ ('x_axis_format', 'y_axis_format'),
+ ('x_axis_showminmax'),
+ ('line_interpolation', 'stacked_style'),
+ ), }] + [NVD3TimeSeriesViz.fieldsets[2]]
+
+
+class DistributionPieViz(NVD3Viz):
+ viz_type = "pie"
+ verbose_name = "Distribution - NVD3 - Pie Chart"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'metrics', 'groupby',
+ 'limit',
+ ('donut', 'show_legend'),
+ )
+ },)
+
+ def query_obj(self):
+ d = super(DistributionPieViz, self).query_obj()
+ d['is_timeseries'] = False
+ return d
+
+ def get_df(self, query_obj=None):
+ df = super(DistributionPieViz, self).get_df(query_obj)
+ df = df.pivot_table(
+ index=self.groupby,
+ values=[self.metrics[0]])
+ df = df.sort(self.metrics[0], ascending=False)
+ return df
+
+ def get_json_data(self):
+ df = self.get_df()
+ df = df.reset_index()
+ df.columns = ['x', 'y']
+ return dumps(df.to_dict(orient="records"))
+
+
+class DistributionBarViz(DistributionPieViz):
+ viz_type = "dist_bar"
+ verbose_name = "Distribution - Bar Chart"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': 'Chart Options',
+ 'fields': (
+ 'groupby',
+ 'columns',
+ 'metrics',
+ 'row_limit',
+ ('show_legend', 'bar_stacked'),
+ )
+ },)
+ form_overrides = {
+ 'groupby': {
+ 'label': 'Series',
+ },
+ 'columns': {
+ 'label': 'Breakdowns',
+ 'description': "Defines how each series is broken down",
+ },
+ }
+
+ def query_obj(self):
+ d = super(DistributionPieViz, self).query_obj() # noqa
+ fd = self.form_data
+ d['is_timeseries'] = False
+ gb = fd.get('groupby') or []
+ cols = fd.get('columns') or []
+ d['groupby'] = set(gb + cols)
+ if len(d['groupby']) < len(gb) + len(cols):
+ raise Exception("Can't have overlap between Series and Breakdowns")
+ if not self.metrics:
+ raise Exception("Pick at least one metric")
+ if not self.groupby:
+ raise Exception("Pick at least one field for [Series]")
+ return d
+
+ def get_df(self, query_obj=None):
+ df = super(DistributionPieViz, self).get_df(query_obj) # noqa
+ fd = self.form_data
+
+ row = df.groupby(self.groupby).sum()[self.metrics[0]].copy()
+ row.sort(ascending=False)
+ columns = fd.get('columns') or []
+ pt = df.pivot_table(
+ index=self.groupby,
+ columns=columns,
+ values=self.metrics)
+ pt = pt.reindex(row.index)
+ return pt
+
+ def get_json_data(self):
+ df = self.get_df()
+ series = df.to_dict('series')
+ chart_data = []
+ for name, ys in series.items():
+ if df[name].dtype.kind not in "biufc":
+ continue
+ if isinstance(name, string_types):
+ series_title = name
+ elif len(self.metrics) > 1:
+ series_title = ", ".join(name)
+ else:
+ l = [str(s) for s in name[1:]]
+ series_title = ", ".join(l)
+ d = {
+ "key": series_title,
+ "values": [
+ {'x': i, 'y': v}
+ for i, v in ys.iteritems()]
+ }
+ chart_data.append(d)
+ return dumps(chart_data)
+
+
+class SunburstViz(BaseViz):
+ viz_type = "sunburst"
+ verbose_name = "Sunburst"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'groupby',
+ 'metric', 'secondary_metric',
+ 'row_limit',
+ )
+ },)
+ form_overrides = {
+ 'metric': {
+ 'label': 'Primary Metric',
+ 'description': (
+ "The primary metric is used to "
+ "define the arc segment sizes"),
+ },
+ 'secondary_metric': {
+ 'label': 'Secondary Metric',
+ 'description': (
+ "This secondary metric is used to "
+ "define the color as a ratio against the primary metric. "
+ "If the two metrics match, color is mapped level groups"),
+ },
+ 'groupby': {
+ 'label': 'Hierarchy',
+ 'description': "This defines the level of the hierarchy",
+ },
+ }
+
+ def get_df(self, query_obj=None):
+ df = super(SunburstViz, self).get_df(query_obj)
+ return df
+
+ def get_json_data(self):
+ df = self.get_df()
+
+ # if m1 == m2 duplicate the metric column
+ cols = self.form_data.get('groupby')
+ metric = self.form_data.get('metric')
+ secondary_metric = self.form_data.get('secondary_metric')
+ if metric == secondary_metric:
+ ndf = df[cols]
+ ndf['m1'] = df[metric]
+ ndf['m2'] = df[metric]
+ else:
+ cols += [
+ self.form_data['metric'], self.form_data['secondary_metric']]
+ ndf = df[cols]
+ return ndf.to_json(orient="values")
+
+ def query_obj(self):
+ qry = super(SunburstViz, self).query_obj()
+ qry['metrics'] = [
+ self.form_data['metric'], self.form_data['secondary_metric']]
+ return qry
+
+
+class SankeyViz(BaseViz):
+ viz_type = "sankey"
+ verbose_name = "Sankey"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'groupby',
+ 'metric',
+ 'row_limit',
+ )
+ },)
+ form_overrides = {
+ 'groupby': {
+ 'label': 'Source / Target',
+ 'description': "Choose a source and a target",
+ },
+ }
+
+ def query_obj(self):
+ qry = super(SankeyViz, self).query_obj()
+ if len(qry['groupby']) != 2:
+ raise Exception("Pick exactly 2 columns as [Source / Target]")
+ qry['metrics'] = [
+ self.form_data['metric']]
+ return qry
+
+ def get_json_data(self):
+ df = self.get_df()
+ df.columns = ['source', 'target', 'value']
+ d = df.to_dict(orient='records')
+ return dumps(d)
+
+
+class DirectedForceViz(BaseViz):
+ viz_type = "directed_force"
+ verbose_name = "Directed Force Layout"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'groupby',
+ 'metric',
+ 'row_limit',
+ )
+ },
+ {
+ 'label': 'Force Layout',
+ 'fields': (
+ 'link_length',
+ 'charge',
+ )
+ },)
+ form_overrides = {
+ 'groupby': {
+ 'label': 'Source / Target',
+ 'description': "Choose a source and a target",
+ },
+ }
+ def query_obj(self):
+ qry = super(DirectedForceViz, self).query_obj()
+ if len(self.form_data['groupby']) != 2:
+ raise Exception("Pick exactly 2 columns to 'Group By'")
+ qry['metrics'] = [self.form_data['metric']]
+ return qry
+
+ def get_json_data(self):
+ df = self.get_df()
+ df.columns = ['source', 'target', 'value']
+ d = df.to_dict(orient='records')
+ return dumps(d)
+
+
+class WorldMapViz(BaseViz):
+ viz_type = "world_map"
+ verbose_name = "World Map"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'entity',
+ 'country_fieldtype',
+ 'metric',
+ )
+ },
+ {
+ 'label': 'Bubbles',
+ 'fields': (
+ ('show_bubbles', None),
+ 'secondary_metric',
+ 'max_bubble_size',
+ )
+ })
+ form_overrides = {
+ 'entity': {
+ 'label': 'Country Field',
+ 'description': "3 letter code of the country",
+ },
+ 'metric': {
+ 'label': 'Metric for color',
+ 'description': ("Metric that defines the color of the country"),
+ },
+ 'secondary_metric': {
+ 'label': 'Bubble size',
+ 'description': ("Metric that defines the size of the bubble"),
+ },
+ }
+ def query_obj(self):
+ qry = super(WorldMapViz, self).query_obj()
+ qry['metrics'] = [
+ self.form_data['metric'], self.form_data['secondary_metric']]
+ qry['groupby'] = [self.form_data['entity']]
+ return qry
+
+ def get_json_data(self):
+ from dashed.data import countries
+ df = self.get_df()
+ cols = [self.form_data.get('entity')]
+ metric = self.form_data.get('metric')
+ secondary_metric = self.form_data.get('secondary_metric')
+ if metric == secondary_metric:
+ ndf = df[cols]
+ ndf['m1'] = df[metric]
+ ndf['m2'] = df[metric]
+ else:
+ cols += [metric, secondary_metric]
+ ndf = df[cols]
+ df = ndf
+ df.columns = ['country', 'm1', 'm2']
+ d = df.to_dict(orient='records')
+ for row in d:
+ country = countries.get(
+ self.form_data.get('country_fieldtype'), row['country'])
+ if country:
+ row['country'] = country['cca3']
+ row['latitude'] = country['lat']
+ row['longitude'] = country['lng']
+ row['name'] = country['name']
+ else:
+ row['country'] = "XXX"
+ return dumps(d)
+
+
+class FilterBoxViz(BaseViz):
+ viz_type = "filter_box"
+ verbose_name = "Filters"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'groupby',
+ 'metric',
+ )
+ },)
+ form_overrides = {
+ 'groupby': {
+ 'label': 'Filter fields',
+ 'description': "The fields you want to filter on",
+ },
+ }
+ def query_obj(self):
+ qry = super(FilterBoxViz, self).query_obj()
+ groupby = self.form_data['groupby']
+ if len(groupby) < 1:
+ raise Exception("Pick at least one filter field")
+ qry['metrics'] = [
+ self.form_data['metric']]
+ return qry
+
+ def get_df(self, query_obj=None):
+ qry = self.query_obj()
+
+ filters = [g for g in qry['groupby']]
+ d = {}
+ for flt in filters:
+ qry['groupby'] = [flt]
+ df = super(FilterBoxViz, self).get_df(qry)
+ d[flt] = [
+ {'id': row[0],
+ 'text': row[0],
+ 'filter': flt,
+ 'metric': row[1]}
+ for row in df.itertuples(index=False)]
+ return d
+
+ def get_json_data(self):
+ d = self.get_df()
+ return dumps(d)
+
+
+class IFrameViz(BaseViz):
+ viz_type = "iframe"
+ verbose_name = "iFrame"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': ('url',)
+ },)
+
+
+class ParallelCoordinatesViz(BaseViz):
+ viz_type = "para"
+ verbose_name = "Parallel Coordinates"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'series',
+ 'metrics',
+ 'secondary_metric',
+ 'limit',
+ ('show_datatable', None),
+ )
+ },)
+ def query_obj(self):
+ d = super(ParallelCoordinatesViz, self).query_obj()
+ fd = self.form_data
+ d['metrics'] = fd.get('metrics')
+ second = fd.get('secondary_metric')
+ if second not in d['metrics']:
+ d['metrics'] += [second]
+ d['groupby'] = [fd.get('series')]
+ return d
+
+ def get_json_data(self):
+ df = self.get_df()
+ df = df[[self.form_data.get('series')] + self.form_data.get('metrics')]
+ return df.to_json(orient="records")
+
+class HeatmapViz(BaseViz):
+ viz_type = "heatmap"
+ verbose_name = "Heatmap"
+ is_timeseries = False
+ fieldsets = (
+ {
+ 'label': None,
+ 'fields': (
+ 'all_columns_x',
+ 'all_columns_y',
+ 'metric',
+ )
+ },
+ {
+ 'label': 'Heatmap Options',
+ 'fields': (
+ 'linear_color_scheme',
+ ('xscale_interval', 'yscale_interval'),
+ 'canvas_image_rendering',
+ 'normalize_across',
+ )
+ },)
+ def query_obj(self):
+ d = super(HeatmapViz, self).query_obj()
+ fd = self.form_data
+ d['metrics'] = [fd.get('metric')]
+ d['groupby'] = [fd.get('all_columns_x'), fd.get('all_columns_y')]
+ return d
+
+ def get_json_data(self):
+ df = self.get_df()
+ fd = self.form_data
+ x = fd.get('all_columns_x')
+ y = fd.get('all_columns_y')
+ v = fd.get('metric')
+ if x == y:
+ df.columns = ['x', 'y', 'v']
+ else:
+ df = df[[x, y, v]]
+ df.columns = ['x', 'y', 'v']
+ norm = fd.get('normalize_across')
+ overall = False
+ if norm == 'heatmap':
+ overall = True
+ else:
+ gb = df.groupby(norm, group_keys=False)
+ if len(gb) <= 1:
+ overall = True
+ else:
+ df['perc'] = (
+ gb.apply(
+ lambda x: (x.v - x.v.min()) / (x.v.max() - x.v.min()))
+ )
+ if overall:
+ v = df.v
+ min_ = v.min()
+ df['perc'] = (v - min_) / (v.max() - min_)
+ return df.to_json(orient="records")
+
+
+viz_types_list = [
+ TableViz,
+ PivotTableViz,
+ NVD3TimeSeriesViz,
+ NVD3CompareTimeSeriesViz,
+ NVD3TimeSeriesStackedViz,
+ NVD3TimeSeriesBarViz,
+ DistributionBarViz,
+ DistributionPieViz,
+ BubbleViz,
+ MarkupViz,
+ WordCloudViz,
+ BigNumberViz,
+ SunburstViz,
+ DirectedForceViz,
+ SankeyViz,
+ WorldMapViz,
+ FilterBoxViz,
+ IFrameViz,
+ ParallelCoordinatesViz,
+ HeatmapViz,
+]
+
+viz_types = OrderedDict([(v.viz_type, v) for v in viz_types_list])
diff --git a/docs/Makefile b/docs/Makefile
index 56f29876d..7b78c404d 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -87,9 +87,9 @@ qthelp:
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/panoramix.qhcp"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dashed.qhcp"
@echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/panoramix.qhc"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dashed.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@@ -104,8 +104,8 @@ devhelp:
@echo
@echo "Build finished."
@echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/panoramix"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/panoramix"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/dashed"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dashed"
@echo "# devhelp"
epub:
diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html
index 7bb04b52f..8dc2374d4 100644
--- a/docs/_templates/layout.html
+++ b/docs/_templates/layout.html
@@ -21,7 +21,7 @@
-
Panoramix
+
Dashed
an open source data visualization platform
@@ -80,7 +80,7 @@
-
Panoramix
+
Dashed
is an open source data visualization platform that provides easy
exploration of your data and allows you to create and share
diff --git a/docs/build.sh b/docs/build.sh
index 611e42d08..20120b160 100755
--- a/docs/build.sh
+++ b/docs/build.sh
@@ -1,4 +1,4 @@
#!/usr/bin/env bash
rm -r _build
make html
-cp -r _build/html/ ../../panoramix-docs/
+cp -r _build/html/ ../../dashed-docs/
diff --git a/docs/conf.py b/docs/conf.py
index 70255cc68..f610a7ad2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
-# panoramix documentation build configuration file, created by
+# dashed documentation build configuration file, created by
# sphinx-quickstart on Thu Dec 17 15:42:06 2015.
#
# This file is execfile()d with the current directory set to its
@@ -50,7 +50,7 @@ source_suffix = '.rst'
master_doc = 'index'
# General information about the project.
-project = u'panoramix'
+project = u'dashed'
copyright = u'2015, Maxime Beauchemin, Airbnb'
author = u'Maxime Beauchemin'
@@ -120,7 +120,7 @@ html_theme_path = sphinx_bootstrap_theme.get_html_theme_path()
# documentation.
html_theme_options = {
#'bootswatch_theme': 'simplex',
- 'navbar_title': 'Panoramix Documentation',
+ 'navbar_title': 'Dashed Documentation',
'navbar_fixed_top': "false",
#'navbar_class': "navbar navbar-default",
}
@@ -210,7 +210,7 @@ html_show_sourcelink = False
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
-htmlhelp_basename = 'panoramixdoc'
+htmlhelp_basename = 'dasheddoc'
# -- Options for LaTeX output ---------------------------------------------
@@ -232,7 +232,7 @@ latex_elements = {
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
- (master_doc, 'panoramix.tex', u'Panoramix Documentation',
+ (master_doc, 'dashed.tex', u'Dashed Documentation',
u'Maxime Beauchemin', 'manual'),
]
@@ -262,7 +262,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
- (master_doc, 'Panoramix', u'panoramix Documentation',
+ (master_doc, 'Dashed', u'dashed Documentation',
[author], 1)
]
@@ -276,8 +276,8 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
- (master_doc, 'Panoramix', u'Panoramix Documentation',
- author, 'Panoramix', 'One line description of project.',
+ (master_doc, 'Dashed', u'Dashed Documentation',
+ author, 'Dashed', 'One line description of project.',
'Miscellaneous'),
]
diff --git a/docs/index.rst b/docs/index.rst
index 7817e26e7..026bf737a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1,5 +1,3 @@
-.. image:: img/tux_panoramix.png
-
Overview
=======================================
diff --git a/docs/user_guide.rst b/docs/user_guide.rst
index d674a2979..e6a2fa709 100644
--- a/docs/user_guide.rst
+++ b/docs/user_guide.rst
@@ -2,11 +2,11 @@ User Guide
==========
The user guide is a collection of short videos showing different aspect
-of Panoramix.
+of Dashed.
Quick Intro
'''''''''''
-This video demonstrates how Panoramix works at a high level, it shows how
+This video demonstrates how Dashed works at a high level, it shows how
to navigate through datasets and dashboards that are already available.
- Coming soon!
@@ -41,7 +41,7 @@ to toggle them on dashboards.
Adding a Table
''''''''''''''
-This videos shows you how to expose a new table in Panoramix, and how to
+This videos shows you how to expose a new table in Dashed, and how to
define the semantics on how this can be accessed by others in the ``Explore``
and ``Dashboard`` views.
diff --git a/run_tests.sh b/run_tests.sh
index 080c2c764..3bec789b9 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-rm /tmp/panoramix_unittests.db
-export PANORAMIX_CONFIG=tests.panoramix_test_config
-panoramix/bin/panoramix db upgrade
-nosetests tests/core_tests.py --with-coverage --cover-package=panoramix -v
+rm /tmp/dashed_unittests.db
+export DASHED_CONFIG=tests.dashed_test_config
+dashed/bin/dashed db upgrade
+nosetests tests/core_tests.py --with-coverage --cover-package=dashed -v
diff --git a/setup.cfg b/setup.cfg
index 24ec6b782..e46ec26cd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
[metadata]
-name = Panoramix
+name = Dashed
summary = a data exploration platform
description-file = README.md
author = Maxime Beauchemin
@@ -7,7 +7,7 @@ author-email = maximebeauchemin@gmail.com
license = Apache License, Version 2.0
[files]
-packages = panoramix
+packages = dashed
[build_sphinx]
source-dir = docs/
diff --git a/setup.py b/setup.py
index e1a071cbc..b79b2ff75 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,7 @@ from setuptools import setup, find_packages
version = '0.8.0'
setup(
- name='panoramix',
+ name='dashed',
description=(
"A interactive data visualization platform build on SqlAlchemy "
"and druid.io"),
@@ -11,7 +11,7 @@ setup(
packages=find_packages(),
include_package_data=True,
zip_safe=False,
- scripts=['panoramix/bin/panoramix'],
+ scripts=['dashed/bin/dashed'],
install_requires=[
'alembic>=0.8.2, <0.9.0',
'cryptography>=1.1.1, <2.0.0',
@@ -36,7 +36,7 @@ setup(
],
author='Maxime Beauchemin',
author_email='maximebeauchemin@gmail.com',
- url='https://github.com/mistercrunch/panoramix',
+ url='https://github.com/airbnb/dashed',
download_url=(
- 'https://github.com/mistercrunch/panoramix/tarball/' + version),
+ 'https://github.com/airbnb/dashed/tarball/' + version),
)
diff --git a/tests/core_tests.py b/tests/core_tests.py
index 8fae4f55d..dd936bb86 100644
--- a/tests/core_tests.py
+++ b/tests/core_tests.py
@@ -2,13 +2,13 @@ import imp
import doctest
import os
import unittest
-os.environ['PANORAMIX_CONFIG'] = 'tests.panoramix_test_config'
+os.environ['DASHED_CONFIG'] = 'tests.dashed_test_config'
from flask.ext.testing import LiveServerTestCase, TestCase
-import panoramix
-from panoramix import app, db, models, utils
+import dashed
+from dashed import app, db, models, utils
BASE_DIR = app.config.get("BASE_DIR")
-cli = imp.load_source('cli', BASE_DIR + "/bin/panoramix")
+cli = imp.load_source('cli', BASE_DIR + "/bin/dashed")
class LiveTest(TestCase):
@@ -22,7 +22,7 @@ class LiveTest(TestCase):
pass
def test_init(self):
- utils.init(panoramix)
+ utils.init(dashed)
def test_load_examples(self):
cli.load_examples(sample=True)
@@ -39,7 +39,7 @@ class LiveTest(TestCase):
self.client.get(viz.get_json())
def test_csv(self):
- self.client.get('/panoramix/explore/table/1/?viz_type=table&granularity=ds&since=100+years&until=now&metrics=count&groupby=name&limit=50&show_brush=y&show_brush=false&show_legend=y&show_brush=false&rich_tooltip=y&show_brush=false&show_brush=false&show_brush=false&show_brush=false&y_axis_format=&x_axis_showminmax=y&show_brush=false&line_interpolation=linear&rolling_type=None&rolling_periods=&time_compare=&num_period_compare=&where=&having=&flt_col_0=gender&flt_op_0=in&flt_eq_0=&flt_col_0=gender&flt_op_0=in&flt_eq_0=&slice_id=14&slice_name=Boys&collapsed_fieldsets=&action=&datasource_name=birth_names&datasource_id=1&datasource_type=table&previous_viz_type=line&csv=true')
+ self.client.get('/dashed/explore/table/1/?viz_type=table&granularity=ds&since=100+years&until=now&metrics=count&groupby=name&limit=50&show_brush=y&show_brush=false&show_legend=y&show_brush=false&rich_tooltip=y&show_brush=false&show_brush=false&show_brush=false&show_brush=false&y_axis_format=&x_axis_showminmax=y&show_brush=false&line_interpolation=linear&rolling_type=None&rolling_periods=&time_compare=&num_period_compare=&where=&having=&flt_col_0=gender&flt_op_0=in&flt_eq_0=&flt_col_0=gender&flt_op_0=in&flt_eq_0=&slice_id=14&slice_name=Boys&collapsed_fieldsets=&action=&datasource_name=birth_names&datasource_id=1&datasource_type=table&previous_viz_type=line&csv=true')
def test_dashboard(self):
for dash in db.session.query(models.Dashboard).all():
diff --git a/tests/dashed_test_config.py b/tests/dashed_test_config.py
new file mode 100644
index 000000000..f61541e88
--- /dev/null
+++ b/tests/dashed_test_config.py
@@ -0,0 +1,6 @@
+from dashed.config import *
+
+AUTH_USER_REGISTRATION_ROLE = 'alpha'
+SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/dashed_unittests.db'
+DEBUG = True
+DASHED_WEBSERVER_PORT = 8081