diff options
| author | Chirag Dodiya (OpenERP) <cod@tinyerp.com> | 2014-04-25 11:49:36 +0530 |
|---|---|---|
| committer | Chirag Dodiya (OpenERP) <cod@tinyerp.com> | 2014-04-25 11:49:36 +0530 |
| commit | 616c20a0f7d5d34aac864cd5daf76e88690b7f35 (patch) | |
| tree | b45add97fcf9d582525c318c63f4e687353b7e53 /addons/web_graph/static/src | |
| parent | 0abd187bec3126e917959b1c7b67082f98757ce4 (diff) | |
| parent | 8ddf7a21b7c4f202440640aea724fbe8590f8db9 (diff) | |
[MRG]Merge lp:openerp-web
bzr revid: cod@tinyerp.com-20140425061936-csz156gyzu0holkw
Diffstat (limited to 'addons/web_graph/static/src')
| -rw-r--r-- | addons/web_graph/static/src/css/flotr2.css | 69 | ||||
| -rw-r--r-- | addons/web_graph/static/src/css/graph.css | 204 | ||||
| -rw-r--r-- | addons/web_graph/static/src/css/nv.d3.css | 769 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/graph.js | 434 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/graph_view.js | 236 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/graph_widget.js | 777 | ||||
| -rw-r--r-- | addons/web_graph/static/src/js/pivot_table.js | 450 | ||||
| -rw-r--r-- | addons/web_graph/static/src/xml/web_graph.xml | 140 |
8 files changed, 2463 insertions, 616 deletions
diff --git a/addons/web_graph/static/src/css/flotr2.css b/addons/web_graph/static/src/css/flotr2.css deleted file mode 100644 index fae7b033811..00000000000 --- a/addons/web_graph/static/src/css/flotr2.css +++ /dev/null @@ -1,69 +0,0 @@ -/* Flotr Styles */ - -.flotr-datagrid-container { - border: 1px solid #999; - border-bottom: none; - background: #fff; -} -.flotr-datagrid { - border-collapse: collapse; - border-spacing: 0; -} -.flotr-datagrid td, .flotr-datagrid th { - border: 1px solid #ccc; - padding: 1px 3px; - min-width: 2em; -} -.flotr-datagrid tr:hover, .flotr-datagrid col.hover { - background: #f3f3f3; -} -.flotr-datagrid tr:hover th, .flotr-datagrid th.hover { - background: #999; - color: #fff; -} -.flotr-datagrid th { - text-align: left; - background: #e3e3e3; - border: 2px outset #fff; -} -.flotr-datagrid-toolbar { - padding: 1px; - border-bottom: 1px solid #ccc; - background: #f9f9f9; -} -.flotr-datagrid td:hover { - background: #ccc; -} -.flotr-datagrid .first-row th { - text-align: center; -} -.flotr-canvas { - margin-bottom: -3px; - padding-bottom: 1px; -} -.flotr-tabs-group { - border-top: 1px solid #999; -} -.flotr-tab { - border: 1px solid #666; - border-top: none; - margin: 0 3px; - padding: 1px 4px; - cursor: pointer; - -moz-border-radius: 0 0 4px 4px; - -webkit-border-bottom-left-radius: 4px; - -webkit-border-bottom-right-radius: 4px; - border-radius: 0 0 4px 4px; - opacity: 0.5; - -moz-opacity: 0.5; -} -.flotr-tab.selected { - background: #ddd; - opacity: 1; - -moz-opacity: 1; -} -.flotr-tab:hover { - background: #ccc; -} - - diff --git a/addons/web_graph/static/src/css/graph.css b/addons/web_graph/static/src/css/graph.css index 2fd4b5953cf..25ee5db9594 100644 --- a/addons/web_graph/static/src/css/graph.css +++ b/addons/web_graph/static/src/css/graph.css @@ -1,80 +1,130 @@ -.openerp a.dropdown-menu-icon { - z-index: 1; - position: absolute; - color: #4c4c4c; - right: 8px; -} -.openerp a.dropdown-menu-icon:hover { - text-decoration: none; -} -.editor-render { - position: relative; -} -#editor-render-body { - position: relative; - width: 650px; - height: 350px; -} -.openerp .graph-dropdown { - display: none; - position: absolute; - top: 32px; - right: 8px; - padding: 8px; - border: 1px solid #afafb6; - background: white; - z-index: 1; - min-width: 160px; - overflow-x: hidden; - -moz-border-radius: 3px; - -webkit-border-radius: 3px; - border-radius: 3px; - -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - -box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} -.openerp .graph-dropdown h3 { - margin: 8px 4px 4px 0; - color: #8786b7; + /* + This is a hack for IE9 ... + Attrs : width and height are not take in consideration + */ +.openerp svg:not(:root) { + overflow: visible; +} + +.graph_pivot_mode { + position:initial; +} + +.graph_chart_mode { + position:relative; +} + +.graph_main_content td { + font-size: 12px; + margin: 45px; + border-collapse: collapse; + vertical-align: middle; + border-bottom: 1px solid #ccc; + padding: 6px 8px; + text-align: right; + color: black; +} + +.graph_main_content td:first-child { + background-color: rgb(240, 238, 238); +} + +.graph_main_content table { + margin-top: 10px; +} + +.graph_measure_selection .oe_selected { + padding-left: 0; +} + +.graph_measure_selection .oe_selected:before { + content: "\f00c"; + width: 20px; + display: inline-block; + font-family: FontAwesome; +} + +.graph_measure_selection li { + margin-left: 5px; +} + +.graph_main_content th { font-size: 13px; + font-weight: normal; + padding: 10px 8px; + vertical-align: top; + color: black; } -.openerp .graph-menu > li > a { - position: relative; - display: block; - padding: 2px 4px 2px 20px; - line-height: 12px; - color: #4c4c4c; - text-decoration: none; -} -.openerp .graph-menu > li > a:hover { - text-decoration: none; - background: #f0f0fa; - background: -moz-linear-gradient(#f0f0fa, #eeeef6); - background: -webkit-gradient(linear, left top, left bottom, from(#f0f0fa), to(#eeeef6)); - background: -webkit-linear-gradient(#f0f0fa, #eeeef6); - -moz-box-shadow: none; - -webkit-box-shadow: none; - -box-shadow: none; -} -.openerp .open .graph-dropdown { - display: block; -} - -.openerp .graph-menu { - padding: 0; - margin: 0; -} -.openerp .graph-menu .active:before { - content: "W"; - font-family: "entypoRegular" !important; - font-size: 24px; - font-weight: 300 !important; - color: #a3a3a3; - position: absolute; - left: 4px; - top: -2px; -} -.openerp .graph-menu li { - padding: 0; - list-style-type: none; + +.graph_main_content tbody td{ + border: 1px solid #b8b8b8; +} + +.graph_main_content tbody { + border-top: 1px solid black; +} + +.graph_main_content thead { + background-color: rgb(240, 238, 238); + border: 1px solid #b8b8b8; +} + +.graph_main_content thead th { + border-left: 1px solid #b8b8b8; +} + +div.graph_widget { + padding:10px; +} + +.oe_dashboard div.graph_widget { + padding:0; +} + +td.graph_border { + text-align: left; + white-space: nowrap; +} + +.graph_main_content table:not(.heatmap) tr:hover td:not(:first-child) +{ + background-color: rgb(245, 245, 245); +} + +span.field-selection { + display: none; + padding: 0; +} + +span.web_graph_indent { + padding-left: 30px; +} + +.web_graph_click:hover { + cursor: pointer; +} + +.graph_widget .dropdown-menu { + font-size: 13px; + padding: 4px 0; + background: #e8e8e8 !important; + border: 1px solid rgba(0, 0, 0, 0.15); + font-color: !important black; + text-shadow: none; + background-color: #e8e8e8; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +.graph_widget .dropdown-menu li a, .graph_widget .dropdown-menu li a:hover, .graph_widget .dropdown-menu li a:focus { + color: black; + text-decoration: none; +} + +th.measure_row { + color: black; + font-size: 12px; + padding:0px 5px 0px 5px ; + margin:0; } diff --git a/addons/web_graph/static/src/css/nv.d3.css b/addons/web_graph/static/src/css/nv.d3.css new file mode 100644 index 00000000000..cae834827cc --- /dev/null +++ b/addons/web_graph/static/src/css/nv.d3.css @@ -0,0 +1,769 @@ + +/******************** + * HTML CSS + */ + + +.chartWrap { + margin: 0; + padding: 0; + overflow: hidden; +} + +/******************** + Box shadow and border radius styling +*/ +.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip { + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2); + + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + +/******************** + * TOOLTIP CSS + */ + +.nvtooltip { + position: absolute; + background-color: rgba(255,255,255,1.0); + padding: 1px; + border: 1px solid rgba(0,0,0,.2); + z-index: 10000; + + font-family: Arial; + font-size: 13px; + text-align: left; + pointer-events: none; + + white-space: nowrap; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +/*Give tooltips that old fade in transition by + putting a "with-transitions" class on the container div. +*/ +.nvtooltip.with-transitions, .with-transitions .nvtooltip { + transition: opacity 250ms linear; + -moz-transition: opacity 250ms linear; + -webkit-transition: opacity 250ms linear; + + transition-delay: 250ms; + -moz-transition-delay: 250ms; + -webkit-transition-delay: 250ms; +} + +.nvtooltip.x-nvtooltip, +.nvtooltip.y-nvtooltip { + padding: 8px; +} + +.nvtooltip h3 { + margin: 0; + padding: 4px 14px; + line-height: 18px; + font-weight: normal; + background-color: rgba(247,247,247,0.75); + text-align: center; + + border-bottom: 1px solid #ebebeb; + + -webkit-border-radius: 5px 5px 0 0; + -moz-border-radius: 5px 5px 0 0; + border-radius: 5px 5px 0 0; +} + +.nvtooltip p { + margin: 0; + padding: 5px 14px; + text-align: center; +} + +.nvtooltip span { + display: inline-block; + margin: 2px 0; +} + +.nvtooltip table { + margin: 6px; + border-spacing:0; +} + + +.nvtooltip table td { + padding: 2px 9px 2px 0; + vertical-align: middle; +} + +.nvtooltip table td.key { + font-weight:normal; +} +.nvtooltip table td.value { + text-align: right; + font-weight: bold; +} + +.nvtooltip table tr.highlight td { + padding: 1px 9px 1px 0; + border-bottom-style: solid; + border-bottom-width: 1px; + border-top-style: solid; + border-top-width: 1px; +} + +.nvtooltip table td.legend-color-guide div { + width: 8px; + height: 8px; + vertical-align: middle; +} + +.nvtooltip .footer { + padding: 3px; + text-align: center; +} + + +.nvtooltip-pending-removal { + position: absolute; + pointer-events: none; +} + + +/******************** + * SVG CSS + */ + + +svg { + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + /* Trying to get SVG to act like a greedy block in all browsers */ + display: block; + width:100%; + height:100%; +} + + +svg text { + font: normal 12px Arial; +} + +svg .title { + font: bold 14px Arial; +} + +.nvd3 .nv-background { + fill: white; + fill-opacity: 0; + /* + pointer-events: none; + */ +} + +.nvd3.nv-noData { + font-size: 18px; + font-weight: bold; +} + + +/********** +* Brush +*/ + +.nv-brush .extent { + fill-opacity: .125; + shape-rendering: crispEdges; +} + + + +/********** +* Legend +*/ + +.nvd3 .nv-legend .nv-series { + cursor: pointer; +} + +.nvd3 .nv-legend .disabled circle { + fill-opacity: 0; +} + + + +/********** +* Axes +*/ +.nvd3 .nv-axis { + pointer-events:none; +} + +.nvd3 .nv-axis path { + fill: none; + stroke: #000; + stroke-opacity: .75; + shape-rendering: crispEdges; +} + +.nvd3 .nv-axis path.domain { + stroke-opacity: .75; +} + +.nvd3 .nv-axis.nv-x path.domain { + stroke-opacity: 0; +} + +.nvd3 .nv-axis line { + fill: none; + stroke: #e5e5e5; + shape-rendering: crispEdges; +} + +.nvd3 .nv-axis .zero line, +/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero { + stroke-opacity: .75; +} + +.nvd3 .nv-axis .nv-axisMaxMin text { + font-weight: bold; +} + +.nvd3 .x .nv-axis .nv-axisMaxMin text, +.nvd3 .x2 .nv-axis .nv-axisMaxMin text, +.nvd3 .x3 .nv-axis .nv-axisMaxMin text { + text-anchor: middle +} + + + +/********** +* Brush +*/ + +.nv-brush .resize path { + fill: #eee; + stroke: #666; +} + + + +/********** +* Bars +*/ + +.nvd3 .nv-bars .negative rect { + zfill: brown; +} + +.nvd3 .nv-bars rect { + zfill: steelblue; + fill-opacity: .75; + + transition: fill-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear; +} + +.nvd3 .nv-bars rect.hover { + fill-opacity: 1; +} + +.nvd3 .nv-bars .hover rect { + fill: lightblue; +} + +.nvd3 .nv-bars text { + fill: rgba(0,0,0,0); +} + +.nvd3 .nv-bars .hover text { + fill: rgba(0,0,0,1); +} + + +/********** +* Bars +*/ + +.nvd3 .nv-multibar .nv-groups rect, +.nvd3 .nv-multibarHorizontal .nv-groups rect, +.nvd3 .nv-discretebar .nv-groups rect { + stroke-opacity: 0; + + transition: fill-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear; +} + +.nvd3 .nv-multibar .nv-groups rect:hover, +.nvd3 .nv-multibarHorizontal .nv-groups rect:hover, +.nvd3 .nv-discretebar .nv-groups rect:hover { + fill-opacity: 1; +} + +.nvd3 .nv-discretebar .nv-groups text, +.nvd3 .nv-multibarHorizontal .nv-groups text { + font-weight: bold; + fill: rgba(0,0,0,1); + stroke: rgba(0,0,0,0); +} + +/*********** +* Pie Chart +*/ + +.nvd3.nv-pie path { + stroke-opacity: 0; + transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear; + +} + +.nvd3.nv-pie .nv-slice text { + stroke: #000; + stroke-width: 0; +} + +.nvd3.nv-pie path { + stroke: #fff; + stroke-width: 1px; + stroke-opacity: 1; +} + +.nvd3.nv-pie .hover path { + fill-opacity: .7; +} +.nvd3.nv-pie .nv-label { + pointer-events: none; +} +.nvd3.nv-pie .nv-label rect { + fill-opacity: 0; + stroke-opacity: 0; +} + +/********** +* Lines +*/ + +.nvd3 .nv-groups path.nv-line { + fill: none; + stroke-width: 1.5px; + /* + stroke-linecap: round; + shape-rendering: geometricPrecision; + + transition: stroke-width 250ms linear; + -moz-transition: stroke-width 250ms linear; + -webkit-transition: stroke-width 250ms linear; + + transition-delay: 250ms + -moz-transition-delay: 250ms; + -webkit-transition-delay: 250ms; + */ +} + +.nvd3 .nv-groups path.nv-line.nv-thin-line { + stroke-width: 1px; +} + + +.nvd3 .nv-groups path.nv-area { + stroke: none; + /* + stroke-linecap: round; + shape-rendering: geometricPrecision; + + stroke-width: 2.5px; + transition: stroke-width 250ms linear; + -moz-transition: stroke-width 250ms linear; + -webkit-transition: stroke-width 250ms linear; + + transition-delay: 250ms + -moz-transition-delay: 250ms; + -webkit-transition-delay: 250ms; + */ +} + +.nvd3 .nv-line.hover path { + stroke-width: 6px; +} + +/* +.nvd3.scatter .groups .point { + fill-opacity: 0.1; + stroke-opacity: 0.1; +} + */ + +.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point { + fill-opacity: 0; + stroke-opacity: 0; +} + +.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point { + fill-opacity: .5 !important; + stroke-opacity: .5 !important; +} + + +.with-transitions .nvd3 .nv-groups .nv-point { + transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + -moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear; + +} + +.nvd3.nv-scatter .nv-groups .nv-point.hover, +.nvd3 .nv-groups .nv-point.hover { + stroke-width: 7px; + fill-opacity: .95 !important; + stroke-opacity: .95 !important; +} + + +.nvd3 .nv-point-paths path { + stroke: #aaa; + stroke-opacity: 0; + fill: #eee; + fill-opacity: 0; +} + + + +.nvd3 .nv-indexLine { + cursor: ew-resize; +} + + +/********** +* Distribution +*/ + +.nvd3 .nv-distribution { + pointer-events: none; +} + + + +/********** +* Scatter +*/ + +/* **Attempting to remove this for useVoronoi(false), need to see if it's required anywhere +.nvd3 .nv-groups .nv-point { + pointer-events: none; +} +*/ + +.nvd3 .nv-groups .nv-point.hover { + stroke-width: 20px; + stroke-opacity: .5; +} + +.nvd3 .nv-scatter .nv-point.hover { + fill-opacity: 1; +} + +/* +.nv-group.hover .nv-point { + fill-opacity: 1; +} +*/ + + +/********** +* Stacked Area +*/ + +.nvd3.nv-stackedarea path.nv-area { + fill-opacity: .7; + /* + stroke-opacity: .65; + fill-opacity: 1; + */ + stroke-opacity: 0; + + transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; + -moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; + -webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear; + + /* + transition-delay: 500ms; + -moz-transition-delay: 500ms; + -webkit-transition-delay: 500ms; + */ + +} + +.nvd3.nv-stackedarea path.nv-area.hover { + fill-opacity: .9; + /* + stroke-opacity: .85; + */ +} +/* +.d3stackedarea .groups path { + stroke-opacity: 0; +} + */ + + + +.nvd3.nv-stackedarea .nv-groups .nv-point { + stroke-opacity: 0; + fill-opacity: 0; +} + +/* +.nvd3.nv-stackedarea .nv-groups .nv-point.hover { + stroke-width: 20px; + stroke-opacity: .75; + fill-opacity: 1; +}*/ + + + +/********** +* Line Plus Bar +*/ + +.nvd3.nv-linePlusBar .nv-bar rect { + fill-opacity: .75; +} + +.nvd3.nv-linePlusBar .nv-bar rect:hover { + fill-opacity: 1; +} + + +/********** +* Bullet +*/ + +.nvd3.nv-bullet { font: 10px sans-serif; } +.nvd3.nv-bullet .nv-measure { fill-opacity: .8; } +.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; } +.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; } +.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; } +.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; } +.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; } +.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; } +.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; } +.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; } +.nvd3.nv-bullet .nv-subtitle { fill: #999; } + + +.nvd3.nv-bullet .nv-range { + fill: #bababa; + fill-opacity: .4; +} +.nvd3.nv-bullet .nv-range:hover { + fill-opacity: .7; +} + + + +/********** +* Sparkline +*/ + +.nvd3.nv-sparkline path { + fill: none; +} + +.nvd3.nv-sparklineplus g.nv-hoverValue { + pointer-events: none; +} + +.nvd3.nv-sparklineplus .nv-hoverValue line { + stroke: #333; + stroke-width: 1.5px; + } + +.nvd3.nv-sparklineplus, +.nvd3.nv-sparklineplus g { + pointer-events: all; +} + +.nvd3 .nv-hoverArea { + fill-opacity: 0; + stroke-opacity: 0; +} + +.nvd3.nv-sparklineplus .nv-xValue, +.nvd3.nv-sparklineplus .nv-yValue { + /* + stroke: #666; + */ + stroke-width: 0; + font-size: .9em; + font-weight: normal; +} + +.nvd3.nv-sparklineplus .nv-yValue { + stroke: #f66; +} + +.nvd3.nv-sparklineplus .nv-maxValue { + stroke: #2ca02c; + fill: #2ca02c; +} + +.nvd3.nv-sparklineplus .nv-minValue { + stroke: #d62728; + fill: #d62728; +} + +.nvd3.nv-sparklineplus .nv-currentValue { + /* + stroke: #444; + fill: #000; + */ + font-weight: bold; + font-size: 1.1em; +} + +/********** +* historical stock +*/ + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick { + stroke-width: 2px; +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover { + stroke-width: 4px; +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive { + stroke: #2ca02c; +} + +.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative { + stroke: #d62728; +} + +.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel { + font-weight: bold; +} + +.nvd3.nv-historicalStockChart .nv-dragTarget { + fill-opacity: 0; + stroke: none; + cursor: move; +} + +.nvd3 .nv-brush .extent { + /* + cursor: ew-resize !important; + */ + fill-opacity: 0 !important; +} + +.nvd3 .nv-brushBackground rect { + stroke: #000; + stroke-width: .4; + fill: #fff; + fill-opacity: .7; +} + + + +/********** +* Indented Tree +*/ + + +/** + * TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library + */ +.nvd3.nv-indentedtree .name { + margin-left: 5px; +} + +.nvd3.nv-indentedtree .clickable { + color: #08C; + cursor: pointer; +} + +.nvd3.nv-indentedtree span.clickable:hover { + color: #005580; + text-decoration: underline; +} + + +.nvd3.nv-indentedtree .nv-childrenCount { + display: inline-block; + margin-left: 5px; +} + +.nvd3.nv-indentedtree .nv-treeicon { + cursor: pointer; + /* + cursor: n-resize; + */ +} + +.nvd3.nv-indentedtree .nv-treeicon.nv-folded { + cursor: pointer; + /* + cursor: s-resize; + */ +} + +/********** +* Parallel Coordinates +*/ + +.nvd3 .background path { + fill: none; + stroke: #ccc; + stroke-opacity: .4; + shape-rendering: crispEdges; +} + +.nvd3 .foreground path { + fill: none; + stroke: steelblue; + stroke-opacity: .7; +} + +.nvd3 .brush .extent { + fill-opacity: .3; + stroke: #fff; + shape-rendering: crispEdges; +} + +.nvd3 .axis line, .axis path { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.nvd3 .axis text { + text-shadow: 0 1px 0 #fff; +} + +/**** +Interactive Layer +*/ +.nvd3 .nv-interactiveGuideLine { + pointer-events:none; +} +.nvd3 line.nv-guideline { + stroke: #ccc; +}
\ No newline at end of file diff --git a/addons/web_graph/static/src/js/graph.js b/addons/web_graph/static/src/js/graph.js deleted file mode 100644 index a84bb53fc85..00000000000 --- a/addons/web_graph/static/src/js/graph.js +++ /dev/null @@ -1,434 +0,0 @@ -/*--------------------------------------------------------- - * OpenERP web_graph - *---------------------------------------------------------*/ - -openerp.web_graph = function (instance) { - -var _lt = instance.web._lt; -var _t = instance.web._t; - -// removed ``undefined`` values -var filter_values = function (o) { - var out = {}; - for (var k in o) { - if (!o.hasOwnProperty(k) || o[k] === undefined) { continue; } - out[k] = o[k]; - } - return out; -}; - -instance.web.views.add('graph', 'instance.web_graph.GraphView'); -instance.web_graph.GraphView = instance.web.View.extend({ - template: "GraphView", - display_name: _lt('Graph'), - view_type: "graph", - - init: function(parent, dataset, view_id, options) { - var self = this; - this._super(parent); - this.set_default_options(options); - this.dataset = dataset; - this.view_id = view_id; - - this.mode = "bar"; // line, bar, area, pie, radar - this.orientation = false; // true: horizontal, false: vertical - this.stacked = true; - - this.spreadsheet = false; // Display data grid, allows copy to CSV - this.forcehtml = false; - this.legend = "top"; // top, inside, no - - this.domain = []; - this.context = {}; - this.group_by = []; - - this.graph = null; - }, - view_loading: function(r) { - return this.load_graph(r); - }, - destroy: function () { - if (this.graph) { - this.graph.destroy(); - } - this._super(); - }, - - load_graph: function(fields_view_get) { - // TODO: move to load_view and document - var self = this; - this.fields_view = fields_view_get; - this.$el.addClass(this.fields_view.arch.attrs['class']); - - this.mode = this.fields_view.arch.attrs.type || 'bar'; - this.orientation = this.fields_view.arch.attrs.orientation == 'horizontal'; - - var width = this.$el.parent().width(); - this.$el.css("width", width); - this.container = this.$el.find("#editor-render-body").css({ - width: width, - height: Math.min(500, width * 0.8) - })[0]; - - var graph_render = this.proxy('graph_render'); - this.$el.on('click', '.oe_graph_options a', function (evt) { - var $el = $(evt.target); - - self.graph_render({data: filter_values({ - mode: $el.data('mode'), - legend: $el.data('legend'), - orientation: $el.data('orientation'), - stacked: $el.data('stacked') - })}); - }); - - this.$el.find("#graph_show_data").click(function () { - self.spreadsheet = ! self.spreadsheet; - self.graph_render(); - }); - this.$el.find("#graph_switch").click(function () { - if (self.mode != 'radar') { - self.orientation = ! self.orientation; - } - self.graph_render(); - }); - - this.$el.find("#graph_download").click(function () { - if (self.legend == "top") { self.legend = "inside"; } - self.forcehtml = true; - - self.graph_get_data().done(function (result) { - self.graph_render_all(result).download.saveImage('png'); - }).always(function () { - self.forcehtml = false; - }); - }); - this.trigger('graph_view_loaded', fields_view_get); - }, - - get_format: function (options) { - options = options || {}; - var legend = { - show: this.legend != 'no', - }; - - switch (this.legend) { - case 'top': - legend.noColumns = 4; - legend.container = this.$el.find("div.graph_header_legend")[0]; - break; - case 'inside': - legend.position = 'nw'; - legend.backgroundColor = '#D2E8FF'; - break; - } - - return _.extend({ - legend: legend, - mouse: { - track: true, - relative: true - }, - spreadsheet : { - show: this.spreadsheet, - initialTab: "data" - }, - HtmlText : (options.xaxis && options.xaxis.labelsAngle) ? false : !this.forcehtml, - }, options); - }, - - make_graph: function (mode, container, data) { - if (mode === 'area') { mode = 'line'; } - var format = this.get_format(this['options_' + mode](data)); - return Flotr.draw(container, data.data, format); - }, - - options_bar: function (data) { - var min = _(data.data).chain() - .map(function (record) { - if (record.data.length > 0){ - return _.min(record.data, function (item) { - return item[1]; - })[1]; - } - }).min().value(); - return { - bars : { - show : true, - stacked : this.stacked, - horizontal : this.orientation, - barWidth : 0.7, - lineWidth : 1 - }, - grid : { - verticalLines : this.orientation, - horizontalLines : !this.orientation, - outline : "sw", - }, - yaxis : { - ticks: this.orientation ? data.ticks : false, - min: !this.orientation ? (min < 0 ? min : 0) : null - }, - xaxis : { - labelsAngle: 45, - ticks: this.orientation ? false : data.ticks, - min: this.orientation ? (min < 0 ? min : 0) : null - } - }; - }, - - options_pie: function (data) { - return { - pie : { - show: true - }, - grid : { - verticalLines : false, - horizontalLines : false, - outline : "", - }, - xaxis : {showLabels: false}, - yaxis : {showLabels: false}, - }; - }, - - options_radar: function (data) { - return { - radar : { - show : true, - stacked : this.stacked - }, - grid : { - circular : true, - minorHorizontalLines : true - }, - xaxis : { - ticks: data.ticks - }, - }; - }, - - options_line: function (data) { - return { - lines : { - show : true, - }, - points: { - show: true, - }, - grid : { - verticalLines : this.orientation, - horizontalLines : !this.orientation, - outline : "sw", - }, - yaxis : { - ticks: this.orientation ? data.ticks : false - }, - xaxis : { - labelsAngle: 45, - ticks: this.orientation ? false : data.ticks - } - }; - }, - - graph_get_data: function () { - var model = this.dataset.model, - domain = new instance.web.CompoundDomain(this.domain || []), - context = new instance.web.CompoundContext(this.context || {}), - group_by = this.group_by || [], - view_id = this.view_id || false, - mode = this.mode || 'bar', - orientation = this.orientation || false, - stacked = this.stacked || false; - - var obj = new instance.web.Model(model); - var view_get; - var fields; - var result = []; - var ticks = {}; - - return this.alive(obj.call("fields_view_get", [view_id, 'graph', context]).then(function(tmp) { - view_get = tmp; - fields = view_get['fields']; - var toload = _.select(group_by, function(x) { return fields[x] === undefined }); - if (toload.length >= 1) - return obj.call("fields_get", [toload, context]); - else - return $.when([]); - }).then(function (fields_to_add) { - _.extend(fields, fields_to_add); - - var tree = $($.parseXML(view_get['arch'])); - - var pos = 0; - var xaxis = _.clone(group_by || []); - var yaxis = []; - tree.find("field").each(function() { - var field = $(this); - if (! field.attr("name")) - return; - if ((group_by.length == 0) && ((! pos) || instance.web.py_eval(field.attr('group') || "false"))) { - xaxis.push(field.attr('name')); - } - if (pos && ! instance.web.py_eval(field.attr('group') || "false")) { - yaxis.push(field.attr('name')); - } - pos += 1; - }); - - if (xaxis.length === 0) - throw new Error("No field for the X axis!"); - if (yaxis.length === 0) - throw new Error("No field for the Y axis!"); - - // Convert a field's data into a displayable string - - function _convert_key(field, data) { - if (fields[field]['type'] === 'many2one') - data = data && data[0]; - return data; - } - - function _convert(field, data, tick) { - tick = tick === undefined ? true : false; - try { - data = instance.web.format_value(data, fields[field]); - } catch(e) { - data = "" + data; - } - if (tick) { - if (ticks[data] === undefined) - ticks[data] = _.size(ticks); - return ticks[data]; - } - return data || 0; - } - - function _orientation(x, y) { - if (! orientation) - return [x, y] - return [y, x] - } - - if (mode === "pie") { - return obj.call("read_group", [domain, yaxis.concat([xaxis[0]]), [xaxis[0]]], {context: context}).then(function(res) { - _.each(res, function(record) { - result.push({ - 'data': [[_convert(xaxis[0], record[xaxis[0]]), record[yaxis[0]]]], - 'label': _convert(xaxis[0], record[xaxis[0]], false) - }); - }); - }); - } else if ((! stacked) || (xaxis.length < 2)) { - var defs = []; - _.each(xaxis, function(x) { - defs.push(obj.call("read_group", [domain, yaxis.concat([x]), [x]], {context: context}).then(function(res) { - return [x, res]; - })); - }); - return $.when.apply($, defs).then(function() { - _.each(_.toArray(arguments), function(res) { - var x = res[0]; - res = res[1]; - result.push({ - 'data': _.map(res, function(record) { - return _orientation(_convert(x, record[x]), record[yaxis[0]] || 0); - }), - 'label': fields[x]['string'] - }); - }); - }); - } else { - xaxis.reverse(); - return obj.call("read_group", [domain, yaxis.concat(xaxis.slice(0, 1)), xaxis.slice(0, 1)], {context: context}).then(function(axis) { - var defs = []; - _.each(axis, function(x) { - var key = x[xaxis[0]] - defs.push(obj.call("read_group", [x['__domain'], yaxis.concat(xaxis.slice(1, 2)), xaxis.slice(1, 2)], {context: context}).then(function(res) { - return [x, key, res]; - })); - }); - return $.when.apply($, defs).then(function() { - _.each(_.toArray(arguments), function(res) { - var x = res[0]; - var key = res[1]; - res = res[2]; - result.push({ - 'data': _.map(res, function(record) { - return _orientation(_convert(xaxis[1], record[xaxis[1]]), record[yaxis[0]] || 0); - }), - 'label': _convert(xaxis[0], key, false) - }) - }); - }); - }); - } - }).then(function() { - var res = { - 'data': result, - 'ticks': _.map(ticks, function(el, key) { return [el, key || _t("Undefined")] }) - }; - return res; - })); - }, - - // Render the graph and update menu styles - graph_render: function (options) { - options = options || {}; - _.extend(this, options.data); - - return this.graph_get_data() - .done(this.proxy('graph_render_all')); - }, - - graph_render_all: function (data) { - var i; - if (this.mode=='area') { - for (i=0; i<data.data.length; i++) { - data.data[i].lines = {fill: true} - } - } - if (this.graph) { - this.graph.destroy(); - } - - // Render the graph - this.$el.find(".graph_header_legend").children().remove(); - this.graph = this.make_graph(this.mode, this.container, data); - - // Update styles of menus - - this.$el.find("a").removeClass("active"); - - var $active = this.$el.find('a[data-mode=' + this.mode + ']'); - if ($active.length > 1) { - $active = $active.filter('[data-stacked=' + this.stacked + ']'); - } - $active = $active.add( - this.$el.find('a:not([data-mode])[data-legend=' + this.legend + ']')); - - $active.addClass('active'); - - if (this.spreadsheet) { - this.$el.find("#graph_show_data").addClass("active"); - } - return this.graph; - }, - - // render the graph using the domain, context and group_by - // calls the 'graph_data_get' python controller to process all data - // TODO: check is group_by should better be in the context - do_search: function(domain, context, group_by) { - this.domain = domain; - this.context = context; - this.group_by = group_by; - - this.graph_render(); - }, - - do_show: function() { - this.do_push_state({}); - return this._super(); - }, -}); -}; diff --git a/addons/web_graph/static/src/js/graph_view.js b/addons/web_graph/static/src/js/graph_view.js new file mode 100644 index 00000000000..25a1f932cc4 --- /dev/null +++ b/addons/web_graph/static/src/js/graph_view.js @@ -0,0 +1,236 @@ +/*--------------------------------------------------------- + * OpenERP web_graph + *---------------------------------------------------------*/ + +/* jshint undef: false */ + +openerp.web_graph = function (instance) { +'use strict'; + +var _lt = instance.web._lt; +var _t = instance.web._t; + +instance.web.views.add('graph', 'instance.web_graph.GraphView'); + +instance.web_graph.GraphView = instance.web.View.extend({ + display_name: _lt('Graph'), + view_type: 'graph', + + // ---------------------------------------------------------------------- + // Init stuff + // ---------------------------------------------------------------------- + init: function(parent, dataset, view_id, options) { + var self = this; + this._super(parent, dataset, view_id, options); + this.dataset = dataset; + this.model = new instance.web.Model(dataset.model, {group_by_no_leaf: true}); + this.search_view = parent.searchview; + this.col_search_field = { + get_context: function() { return { col_group_by: self.graph_widget.get_col_groupbys()};}, + get_domain: function () {}, + get_groupby: function () {}, + }; + }, + + view_loading: function (fields_view_get) { + var self = this, + arch = fields_view_get.arch; + + this.widget_config = { + title: arch.attrs.string, + stacked : (arch.attrs.stacked === 'True'), + mode: arch.attrs.type || 'bar', + measures: [], + row_groupby: [], + col_groupby: [], + graph_view: this, + }; + + _.each(arch.children, function (field) { + var field_name = field.attrs.name; + if (_.has(field.attrs, 'interval')) { + field_name = field.attrs.name + ':' + field.attrs.interval; + } + if (_.has(field.attrs, 'type')) { + switch (field.attrs.type) { + case 'row': + self.widget_config.row_groupby.push(field_name); + break; + case 'col': + self.widget_config.col_groupby.push(field_name); + break; + case 'measure': + self.widget_config.measures.push(field_name); + break; + } + } else { // old style, kept for backward compatibility + if ('operator' in field.attrs) { + self.widget_config.measures.push(field_name); + } else { + self.widget_config.row_groupby.push(field_name); + } + } + }); + if (self.widget_config.measures.length === 0) { + self.widget_config.measures.push('__count'); + } + }, + + do_search: function (domain, context, group_by) { + if (this.ignore_do_search) { + this.ignore_do_search = false; + return; + } + var self = this, + groupbys = this.get_groupbys_from_searchview(), + col_group_by = groupbys.col_group_by; + + if (!this.graph_widget) { + if (group_by.length) { + this.widget_config.row_groupby = group_by; + } + if (col_group_by.length) { + this.widget_config.col_groupby = col_group_by; + } + this.graph_widget = new openerp.web_graph.Graph(this, this.model, domain, this.widget_config); + this.graph_widget.appendTo(this.$el); + this.ViewManager.on('switch_mode', this, function (e) { + if (e === 'graph') { + var group_bys = self.get_groupbys_from_searchview(); + this.graph_widget.set(domain, group_bys.group_by, group_bys.col_group_by); + } + }); + return; + } + + this.graph_widget.set(domain, group_by, col_group_by); + }, + + get_groupbys_from_searchview: function () { + var result = { group_by: [], col_group_by: []}, + searchdata = this.search_view.build_search_data(); + + _.each(searchdata.groupbys, function (data) { + data = (_.isString(data)) ? py.eval(data) : data; + result.group_by = result.group_by.concat(data.group_by); + if (data.col_group_by) { + result.col_group_by = result.col_group_by.concat(data.col_group_by); + } + }); + + if (result.col_group_by.length) { + return result; + } + _.each(searchdata.contexts, function (context) { + if (context.col_group_by) { + result.col_group_by = result.col_group_by.concat(context.col_group_by); + } + }); + return result; + }, + + do_show: function () { + this.do_push_state({}); + return this._super(); + }, + + // ---------------------------------------------------------------------- + // Search view integration + // ---------------------------------------------------------------------- + + // add groupby to the search view + register_groupby: function(row_groupby, col_groupby) { + var query = this.search_view.query, + groupbys = this.get_groupbys_from_searchview(), + search_row_groupby = groupbys.group_by, + search_col_groupby = groupbys.col_group_by, + row_gb_changed = !_.isEqual(_.pluck(row_groupby, 'field'), search_row_groupby), + col_gb_changed = !_.isEqual(_.pluck(col_groupby, 'field'), search_col_groupby); + + if (!_.has(this.search_view, '_s_groupby')) { return; } + + if (!row_gb_changed && !col_gb_changed) { + return; + } + + if (row_gb_changed && col_gb_changed) { + // when two changes to the search view will be done, the method do_search + // will be called twice, once with the correct groupby and incorrect col_groupby, + // and once with correct informations. This flag is necessary to prevent the + // incorrect informations to propagate and trigger useless queries + this.ignore_do_search = true; + } + + if (row_gb_changed) { + // add row groupbys + var row_facet = this.make_row_groupby_facets(row_groupby), + row_search_facet = query.findWhere({category:'GroupBy'}); + + if (row_search_facet) { + row_search_facet.values.reset(row_facet.values); + } else { + if (row_groupby.length) { + query.add(row_facet); + } + } + } + + if (col_gb_changed) { + // add col groupbys + var col_facet = this.make_col_groupby_facets(col_groupby), + col_search_facet = query.findWhere({category:'ColGroupBy'}); + + if (col_search_facet) { + col_search_facet.values.reset(col_facet.values); + } else { + if (col_groupby.length) { + query.add(col_facet); + } + } + } + }, + + make_row_groupby_facets: function(groupbys) { + return { + category:'GroupBy', + values: this.make_groupby_values(groupbys, 'group_by'), + icon:'w', + field: this.search_view._s_groupby + }; + }, + + make_col_groupby_facets: function(groupbys) { + return { + category:'ColGroupBy', + values: this.make_groupby_values(groupbys, 'col_group_by'), + icon:'f', + field: this.col_search_field + }; + }, + + make_groupby_values: function (groupbys, category) { + return _.map(groupbys, function (groupby) { + var context = {}; + context[category] = groupby.field; + var value; + if (category === 'group_by' && groupby.type !== 'date' && groupby.type !== 'datetime') { + value = groupby.filter || {attrs: {domain: [], context: context}}; + } else { + value = {attrs: {domain: [], context: context}}; + } + return { + label: groupby.string, + value: value + }; + }); + }, +}); +}; + + + + + + + + diff --git a/addons/web_graph/static/src/js/graph_widget.js b/addons/web_graph/static/src/js/graph_widget.js new file mode 100644 index 00000000000..317ea6cbfb9 --- /dev/null +++ b/addons/web_graph/static/src/js/graph_widget.js @@ -0,0 +1,777 @@ + +/* jshint undef: false */ + +(function () { +'use strict'; +var QWeb = openerp.web.qweb; +var _lt = openerp.web._lt; +var _t = openerp.web._t; + +nv.dev = false; // sets nvd3 library in production mode + +openerp.web_graph.Graph = openerp.web.Widget.extend({ + template: 'GraphWidget', + + // ---------------------------------------------------------------------- + // Init stuff + // ---------------------------------------------------------------------- + init: function(parent, model, domain, options) { + this._super(parent); + this.model = model; + this.domain = domain; + this.mode = options.mode || 'pivot'; // pivot, bar, pie, line + this.heatmap_mode = options.heatmap_mode || 'none'; + this.visible_ui = options.visible_ui || true; + this.bar_ui = options.bar_ui || 'group'; + this.graph_view = options.graph_view || null; + this.pivot_options = options; + this.title = options.title || 'Data'; + }, + + start: function() { + var self = this; + this.table = $('<table>'); + this.$('.graph_main_content').append(this.table); + + var indexes = {'pivot': 0, 'bar': 1, 'line': 2, 'chart': 3}; + this.$('.graph_mode_selection label').eq(indexes[this.mode]).addClass('active'); + + if (this.mode !== 'pivot') { + this.$('.graph_heatmap label').addClass('disabled'); + this.$('.graph_main_content').addClass('graph_chart_mode'); + } else { + this.$('.graph_main_content').addClass('graph_pivot_mode'); + } + + openerp.session.rpc('/web_graph/check_xlwt').then(function (result) { + self.$('.graph_options_selection label').toggle(result); + }); + + return this.model.call('fields_get', []).then(function (f) { + self.fields = f; + self.fields.__count = {field:'__count', type: 'integer', string:_t('Quantity')}; + self.important_fields = self.get_search_fields(); + self.measure_list = self.get_measures(); + self.add_measures_to_options(); + self.pivot_options.row_groupby = self.create_field_values(self.pivot_options.row_groupby || []); + self.pivot_options.col_groupby = self.create_field_values(self.pivot_options.col_groupby || []); + self.pivot_options.measures = self.create_field_values(self.pivot_options.measures || [{field:'__count', type: 'integer', string:'Quantity'}]); + self.pivot = new openerp.web_graph.PivotTable(self.model, self.domain, self.fields, self.pivot_options); + self.pivot.update_data().then(function () { + self.display_data(); + if (self.graph_view) { + self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby); + } + }); + openerp.web.bus.on('click', self, function () { + if (self.dropdown) { + self.dropdown.remove(); + self.dropdown = null; + } + }); + self.put_measure_checkmarks(); + }); + }, + + // this method gets the fields that appear in the search view, under the + // 'Groupby' heading + get_search_fields: function () { + var self = this, + parent = this.getParent(); + + while (!(parent instanceof openerp.web.ViewManager)) { + parent = parent.getParent(); + } + + var search_view = parent.searchview; + + var groupbygroups = _(search_view.inputs).select(function (g) { + return g instanceof openerp.web.search.GroupbyGroup; + }); + + var filters = _.flatten(_.pluck(groupbygroups, 'filters'), true), + groupbys = _.flatten(_.map(filters, function (filter) { + var groupby = py.eval(filter.attrs.context).group_by; + if (!(groupby instanceof Array)) { groupby = [groupby]; } + return _.map(groupby, function(g) { + return {field: g, filter: filter}; + }); + })); + + return _.uniq(_.map(groupbys, function (groupby) { + var field = groupby.field, + filter = groupby.filter, + raw_field = field.split(':')[0], + string = (field === raw_field) ? filter.attrs.string : self.fields[raw_field].string; + + filter = (field === raw_field) ? filter : undefined; + + return { field: raw_field, string: string, filter: filter }; + }), false, function (filter) {return filter.field;}); + }, + + // Extracts the integer/float fields which are not 'id' + get_measures: function() { + return _.compact(_.map(this.fields, function (f, id) { + if (((f.type === 'integer') || (f.type === 'float')) && (id !== 'id')) { + return {field:id, type: f.type, string: f.string}; + } + })); + }, + + add_measures_to_options: function() { + this.$('.graph_measure_selection').append( + _.map(this.measure_list, function (measure) { + return $('<li>').append($('<a>').attr('data-choice', measure.field) + .attr('href', '#') + .text(measure.string)); + })); + }, + + // ---------------------------------------------------------------------- + // Configuration methods + // ---------------------------------------------------------------------- + set: function (domain, row_groupby, col_groupby) { + if (!this.pivot) { + this.pivot_options.domain = domain; + this.pivot_options.row_groupby = row_groupby; + this.pivot_options.col_groupby = col_groupby; + return; + } + var row_gbs = this.create_field_values(row_groupby), + col_gbs = this.create_field_values(col_groupby), + dom_changed = !_.isEqual(this.pivot.domain, domain), + row_gb_changed = !_.isEqual(row_gbs, this.pivot.rows.groupby), + col_gb_changed = !_.isEqual(col_gbs, this.pivot.cols.groupby), + row_reduced = is_strict_beginning_of(row_gbs, this.pivot.rows.groupby), + col_reduced = is_strict_beginning_of(col_gbs, this.pivot.cols.groupby); + + if (!dom_changed && row_reduced && !col_gb_changed) { + this.pivot.fold_with_depth(this.pivot.rows, row_gbs.length); + this.display_data(); + return; + } + if (!dom_changed && col_reduced && !row_gb_changed) { + this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length); + this.display_data(); + return; + } + + if (!dom_changed && col_reduced && row_reduced) { + this.pivot.fold_with_depth(this.pivot.rows, row_gbs.length); + this.pivot.fold_with_depth(this.pivot.cols, col_gbs.length); + this.display_data(); + return; + } + + if (dom_changed || row_gb_changed || col_gb_changed) { + this.pivot.set(domain, row_gbs, col_gbs).then(this.proxy('display_data')); + } + }, + + set_mode: function (mode) { + this.mode = mode; + + if (mode === 'pivot') { + this.$('.graph_heatmap label').removeClass('disabled'); + this.$('.graph_main_content').removeClass('graph_chart_mode').addClass('graph_pivot_mode'); + } else { + this.$('.graph_heatmap label').addClass('disabled'); + this.$('.graph_main_content').removeClass('graph_pivot_mode').addClass('graph_chart_mode'); + } + this.display_data(); + }, + + set_heatmap_mode: function (mode) { // none, row, col, all + this.heatmap_mode = mode; + if (mode === 'none') { + this.$('.graph_heatmap label').removeClass('disabled'); + this.$('.graph_heatmap label').removeClass('active'); + } + this.display_data(); + }, + + create_field_value: function (f) { + var field = (_.contains(f, ':')) ? f.split(':')[0] : f, + groupby_field = _.findWhere(this.important_fields, {field:field}), + string = groupby_field ? groupby_field.string : this.fields[field].string, + result = {field: f, string: string, type: this.fields[field].type }; + + if (groupby_field) { + result.filter = groupby_field.filter; + } + + return result; + }, + + create_field_values: function (field_ids) { + return _.map(field_ids, this.proxy('create_field_value')); + }, + + + get_col_groupbys: function () { + return _.pluck(this.pivot.cols.groupby, 'field'); + }, + + // ---------------------------------------------------------------------- + // UI code + // ---------------------------------------------------------------------- + events: { + 'click .graph_mode_selection label' : 'mode_selection', + 'click .graph_measure_selection li' : 'measure_selection', + 'click .graph_options_selection label' : 'option_selection', + 'click .graph_heatmap label' : 'heatmap_mode_selection', + 'click .web_graph_click' : 'header_cell_clicked', + 'click a.field-selection' : 'field_selection', + }, + + mode_selection: function (event) { + event.preventDefault(); + var mode = event.currentTarget.getAttribute('data-mode'); + this.set_mode(mode); + }, + + measure_selection: function (event) { + event.preventDefault(); + event.stopPropagation(); + var measure_field = event.target.getAttribute('data-choice'); + var measure = { + field: measure_field, + type: this.fields[measure_field].type, + string: this.fields[measure_field].string + }; + + this.pivot.toggle_measure(measure).then(this.proxy('display_data')); + this.put_measure_checkmarks(); + }, + + put_measure_checkmarks: function () { + var self = this, + measures_li = this.$('.graph_measure_selection a'); + measures_li.removeClass('oe_selected'); + _.each(this.measure_list, function (measure, index) { + if (_.findWhere(self.pivot.measures, measure)) { + measures_li.eq(index).addClass('oe_selected'); + } + }); + + }, + + option_selection: function (event) { + event.preventDefault(); + switch (event.currentTarget.getAttribute('data-choice')) { + case 'bar_grouped': + this.bar_ui = 'group'; + if (this.mode === 'bar') { + this.display_data(); + } + break; + case 'bar_stacked': + this.bar_ui = 'stack'; + if (this.mode === 'bar') { + this.display_data(); + } + break; + case 'swap_axis': + this.swap_axis(); + break; + case 'update_values': + this.pivot.update_data().then(this.proxy('display_data')); + break; + case 'export_data': + this.export_xls(); + break; + } + }, + + heatmap_mode_selection: function (event) { + event.preventDefault(); + var mode = event.currentTarget.getAttribute('data-mode'); + if (this.heatmap_mode === mode) { + event.stopPropagation(); + this.set_heatmap_mode('none'); + } else { + this.set_heatmap_mode(mode); + } + }, + + header_cell_clicked: function (event) { + event.preventDefault(); + event.stopPropagation(); + var id = event.target.getAttribute('data-id'), + header = this.pivot.get_header(id), + self = this; + + if (header.expanded) { + this.fold(header); + return; + } + if (header.path.length < header.root.groupby.length) { + this.expand(id); + return; + } + if (!this.important_fields.length) { + return; + } + + var fields = _.map(this.important_fields, function (field) { + return {id: field.field, value: field.string, type:self.fields[field.field.split(':')[0]].type}; + }); + if (this.dropdown) { + this.dropdown.remove(); + } + this.dropdown = $(QWeb.render('field_selection', {fields:fields, header_id:id})); + $(event.target).after(this.dropdown); + this.dropdown.css({position:'absolute', + left:event.pageX, + top:event.pageY}); + this.$('.field-selection').next('.dropdown-menu').toggle(); + + + }, + + field_selection: function (event) { + var id = event.target.getAttribute('data-id'), + field_id = event.target.getAttribute('data-field-id'), + interval, + groupby = this.create_field_value(field_id); + event.preventDefault(); + if (this.fields[field_id].type === 'date' || this.fields[field_id].type === 'datetime') { + interval = event.target.getAttribute('data-interval'); + groupby.field = groupby.field + ':' + interval; + } + this.expand(id, groupby); + }, + + // ---------------------------------------------------------------------- + // Pivot Table integration + // ---------------------------------------------------------------------- + expand: function (header_id, groupby) { + var self = this, + header = this.pivot.get_header(header_id), + update_groupby = !!groupby; + + groupby = groupby || header.root.groupby[header.path.length]; + + this.pivot.expand(header_id, groupby).then(function () { + if (update_groupby && self.graph_view) { + self.graph_view.register_groupby(self.pivot.rows.groupby, self.pivot.cols.groupby); + } + self.display_data(); + }); + + }, + + fold: function (header) { + var update_groupby = this.pivot.fold(header); + + this.display_data(); + if (update_groupby && this.graph_view) { + this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby); + } + }, + + swap_axis: function () { + this.pivot.swap_axis(); + this.display_data(); + this.graph_view.register_groupby(this.pivot.rows.groupby, this.pivot.cols.groupby); + }, + + // ---------------------------------------------------------------------- + // Convert Pivot data structure into table structure : + // compute rows, cols, colors, cell width, cell height, ... + // ---------------------------------------------------------------------- + build_table: function(raw) { + return { + headers: this.build_headers(), + measure_row: this.build_measure_row(), + rows: this.build_rows(raw), + nbr_measures: this.pivot.measures.length, + title: this.title, + }; + }, + + build_headers: function () { + var pivot = this.pivot, + nbr_measures = pivot.measures.length, + height = _.max(_.map(pivot.cols.headers, function(g) {return g.path.length;})), + rows = []; + + _.each(pivot.cols.headers, function (col) { + if (col.path.length === 0) { return;} + var cell_width = nbr_measures * (col.expanded ? pivot.get_ancestor_leaves(col).length : 1), + cell_height = col.expanded ? 1 : height - col.path.length + 1, + cell = {width: cell_width, height: cell_height, title: col.title, id: col.id, expanded: col.expanded}; + if (rows[col.path.length - 1]) { + rows[col.path.length - 1].push(cell); + } else { + rows[col.path.length - 1] = [cell]; + } + }); + + if (pivot.get_cols_leaves().length > 1) { + rows[0].push({width: nbr_measures, height: height, title: _t('Total'), id: pivot.main_col().id }); + } + if (pivot.cols.headers.length === 1) { + rows = [[{width: nbr_measures, height: 1, title: _t('Total'), id: pivot.main_col().id, expanded: false}]]; + } + return rows; + }, + + build_measure_row: function () { + var nbr_leaves = this.pivot.get_cols_leaves().length, + nbr_cols = nbr_leaves + ((nbr_leaves > 1) ? 1 : 0), + result = [], + add_total = this.pivot.get_cols_leaves().length > 1, + i, m; + for (i = 0; i < nbr_cols; i++) { + for (m = 0; m < this.pivot.measures.length; m++) { + result.push({ + text:this.pivot.measures[m].string, + is_bold: add_total && (i === nbr_cols - 1) + }); + } + } + return result; + }, + + make_cell: function (row, col, value, index, raw) { + var formatted_value = raw && !_.isUndefined(value) ? value : openerp.web.format_value(value, {type:this.pivot.measures[index].type}), + cell = {value:formatted_value}; + + if (this.heatmap_mode === 'none') { return cell; } + var total = (this.heatmap_mode === 'both') ? this.pivot.get_total()[index] + : (this.heatmap_mode === 'row') ? this.pivot.get_total(row)[index] + : this.pivot.get_total(col)[index]; + var color = Math.floor(90 + 165*(total - Math.abs(value))/total); + if (color < 255) { + cell.color = color; + } + return cell; + }, + + build_rows: function (raw) { + var self = this, + pivot = this.pivot, + m, i, cell; + + return _.map(pivot.rows.headers, function (row) { + var cells = []; + var pivot_cells = []; + for (i = 0; i < pivot.cells.length; i++) { + if (pivot.cells[i].x == row.id || pivot.cells[i].y == row.id) { + pivot_cells.push(pivot.cells[i]); + } + } + _.each(pivot.get_cols_leaves(), function (col) { + var values; + for (i = 0; i < pivot_cells.length; i++) { + if (pivot_cells[i].x == col.id || pivot_cells[i].y == col.id) { + values = pivot_cells[i].values; + break; + } + } + if (!values) { values = new Array(pivot.measures.length);} + for (m = 0; m < pivot.measures.length; m++) { + cells.push(self.make_cell(row,col,values[m], m, raw)); + } + }); + if (pivot.get_cols_leaves().length > 1) { + var totals = pivot.get_total(row); + for (m = 0; m < pivot.measures.length; m++) { + cell = self.make_cell(row, pivot.main_col(), totals[m], m, raw); + cell.is_bold = 'true'; + cells.push(cell); + } + } + return { + id: row.id, + indent: row.path.length, + title: row.title, + expanded: row.expanded, + cells: cells, + }; + }); + }, + + // ---------------------------------------------------------------------- + // Main display method + // ---------------------------------------------------------------------- + display_data: function () { + this.$('.graph_main_content svg').remove(); + this.$('.graph_main_content div').remove(); + this.table.empty(); + this.table.toggleClass('heatmap', this.heatmap_mode !== 'none'); + this.width = this.$el.width(); + this.height = Math.min(Math.max(document.documentElement.clientHeight - 116 - 60, 250), Math.round(0.8*this.$el.width())); + + this.$('.graph_header').toggle(this.visible_ui); + if (this.pivot.no_data) { + this.$('.graph_main_content').append($(QWeb.render('graph_no_data'))); + } else { + if (this.mode === 'pivot') { + this.draw_table(); + } else { + this.$('.graph_main_content').append($('<div><svg>')); + this.svg = this.$('.graph_main_content svg')[0]; + this[this.mode](); + } + } + }, + + // ---------------------------------------------------------------------- + // Drawing the table + // ---------------------------------------------------------------------- + draw_table: function () { + var table = this.build_table(); + this.draw_headers(table.headers); + this.draw_measure_row(table.measure_row); + this.draw_rows(table.rows); + }, + + make_header_cell: function (header) { + var cell = (_.has(header, 'cells') ? $('<td>') : $('<th>')) + .addClass('graph_border') + .attr('rowspan', header.height) + .attr('colspan', header.width); + var content = $('<span>').addClass('web_graph_click') + .attr('href','#') + .text(' ' + (header.title || _t('Undefined'))) + .attr('data-id', header.id); + if (_.has(header, 'expanded')) { + content.addClass(header.expanded ? 'fa fa-minus-square' : 'fa fa-plus-square'); + } else { + content.css('font-weight', 'bold'); + } + if (_.has(header, 'indent')) { + for (var i = 0; i < header.indent; i++) { cell.prepend($('<span>', {class:'web_graph_indent'})); } + } + return cell.append(content); + }, + + draw_headers: function (headers) { + var make_cell = this.make_header_cell, + empty_cell = $('<th>').attr('rowspan', headers.length), + thead = $('<thead>'); + + _.each(headers, function (row) { + var html_row = $('<tr>'); + _.each(row, function (header) { + html_row.append(make_cell(header)); + }); + thead.append(html_row); + }); + thead.children(':first').prepend(empty_cell); + this.table.append(thead); + }, + + draw_measure_row: function (measure_row) { + if (this.pivot.measures.length === 1) { return; } + var html_row = $('<tr>').append('<th>'); + _.each(measure_row, function (cell) { + var measure_cell = $('<th>').addClass('measure_row').text(cell.text); + if (cell.is_bold) {measure_cell.css('font-weight', 'bold');} + html_row.append(measure_cell); + }); + this.$('thead').append(html_row); + }, + + draw_rows: function (rows) { + var table = this.table, + make_cell = this.make_header_cell; + + _.each(rows, function (row) { + var html_row = $('<tr>').append(make_cell(row)); + _.each(row.cells, function (cell) { + var html_cell = $('<td>').text(cell.value); + if (_.has(cell, 'color')) { + html_cell.css('background-color', $.Color(255, cell.color, cell.color)); + } + if (cell.is_bold) { html_cell.css('font-weight', 'bold'); } + html_row.append(html_cell); + }); + table.append(html_row); + }); + }, + + // ---------------------------------------------------------------------- + // Drawing charts code + // ---------------------------------------------------------------------- + bar: function () { + var self = this, + dim_x = this.pivot.rows.groupby.length, + dim_y = this.pivot.cols.groupby.length, + show_controls = (this.width > 400 && this.height > 300 && dim_x + dim_y >=2), + data; + + // No groupby + if ((dim_x === 0) && (dim_y === 0)) { + data = [{key: _t('Total'), values:[{ + x: _t('Total'), + y: this.pivot.get_total(), + }]}]; + // Only column groupbys + } else if ((dim_x === 0) && (dim_y >= 1)){ + data = _.map(this.pivot.get_cols_with_depth(1), function (header) { + return { + key: header.title, + values: [{x:header.title, y: self.pivot.get_total(header)[0]}] + }; + }); + // Just 1 row groupby + } else if ((dim_x === 1) && (dim_y === 0)) { + data = _.map(this.pivot.main_row().children, function (pt) { + var value = self.pivot.get_total(pt), + title = (pt.title !== undefined) ? pt.title : _t('Undefined'); + return {x: title, y: value}; + }); + data = [{key: self.pivot.measures[0].string, values:data}]; + // 1 row groupby and some col groupbys + } else if ((dim_x === 1) && (dim_y >= 1)) { + data = _.map(this.pivot.get_cols_with_depth(1), function (colhdr) { + var values = _.map(self.pivot.get_rows_with_depth(1), function (header) { + return { + x: header.title || _t('Undefined'), + y: self.pivot.get_values(header.id, colhdr.id)[0] || 0 + }; + }); + return {key: colhdr.title || _t('Undefined'), values: values}; + }); + // At least two row groupby + } else { + var keys = _.uniq(_.map(this.pivot.get_rows_with_depth(2), function (hdr) { + return hdr.title || _t('Undefined'); + })); + data = _.map(keys, function (key) { + var values = _.map(self.pivot.get_rows_with_depth(1), function (hdr) { + var subhdr = _.find(hdr.children, function (child) { + return ((child.title === key) || ((child.title === undefined) && (key === _t('Undefined')))); + }); + return { + x: hdr.title || _t('Undefined'), + y: (subhdr) ? self.pivot.get_total(subhdr)[0] : 0 + }; + }); + return {key:key, values: values}; + }); + } + + nv.addGraph(function () { + var chart = nv.models.multiBarChart() + .width(self.width) + .height(self.height) + .reduceXTicks(false) + .stacked(self.bar_ui === 'stack') + .showControls(show_controls); + + if (self.width / data[0].values.length < 80) { + chart.rotateLabels(-15); + chart.reduceXTicks(true); + chart.margin({bottom:40}); + } + + d3.select(self.svg) + .datum(data) + .attr('width', self.width) + .attr('height', self.height) + .call(chart); + + nv.utils.windowResize(chart.update); + return chart; + }); + + }, + + line: function () { + var self = this, + dim_x = this.pivot.rows.groupby.length, + dim_y = this.pivot.cols.groupby.length; + + var data = _.map(this.pivot.get_cols_leaves(), function (col) { + var values = _.map(self.pivot.get_rows_with_depth(dim_x), function (row) { + return {x: row.title, y: self.pivot.get_values(row.id,col.id)[0] || 0}; + }); + var title = _.map(col.path, function (p) { + return p || _t('Undefined'); + }).join('/'); + if (dim_y === 0) { + title = self.pivot.measures[0].string; + } + return {values: values, key: title}; + }); + + nv.addGraph(function () { + var chart = nv.models.lineChart() + .x(function (d,u) { return u; }) + .width(self.width) + .height(self.height) + .margin({top: 30, right: 20, bottom: 20, left: 60}); + + d3.select(self.svg) + .attr('width', self.width) + .attr('height', self.height) + .datum(data) + .call(chart); + + return chart; + }); + }, + + pie: function () { + var self = this, + dim_x = this.pivot.rows.groupby.length; + var data = _.map(this.pivot.get_rows_leaves(), function (row) { + var title = _.map(row.path, function (p) { + return p || _t('Undefined'); + }).join('/'); + if (dim_x === 0) { + title = self.measure_label; + } + return {x: title, y: self.pivot.get_total(row)}; + }); + + nv.addGraph(function () { + var chart = nv.models.pieChart() + .color(d3.scale.category10().range()) + .width(self.width) + .height(self.height); + + d3.select(self.svg) + .datum(data) + .transition().duration(1200) + .attr('width', self.width) + .attr('height', self.height) + .call(chart); + + nv.utils.windowResize(chart.update); + return chart; + }); + }, + + // ---------------------------------------------------------------------- + // Controller stuff... + // ---------------------------------------------------------------------- + export_xls: function() { + var c = openerp.webclient.crashmanager; + openerp.web.blockUI(); + this.session.get_file({ + url: '/web_graph/export_xls', + data: {data: JSON.stringify(this.build_table(true))}, + complete: openerp.web.unblockUI, + error: c.rpc_error.bind(c) + }); + }, + +}); + +// Utility function: returns true if the beginning of array2 is array1 and +// if array1 is not array2 +function is_strict_beginning_of (array1, array2) { + if (array1.length >= array2.length) { return false; } + var result = true; + for (var i = 0; i < array1.length; i++) { + if (!_.isEqual(array1[i], array2[i])) { return false;} + } + return result; +} +})(); diff --git a/addons/web_graph/static/src/js/pivot_table.js b/addons/web_graph/static/src/js/pivot_table.js new file mode 100644 index 00000000000..6b12829982e --- /dev/null +++ b/addons/web_graph/static/src/js/pivot_table.js @@ -0,0 +1,450 @@ + +/* jshint undef: false */ + +(function () { +'use strict'; + +var _lt = openerp.web._lt; +var _t = openerp.web._t; + +// PivotTable requires a call to update_data after initialization +openerp.web_graph.PivotTable = openerp.web.Class.extend({ + + init: function (model, domain, fields, options) { + this.cells = []; + this.domain = domain; + this.no_data = true; + this.model = model; + this.fields = fields; + this.fields.__count = {type: 'integer', string:_t('Quantity')}; + this.measures = options.measures || []; + this.rows = { groupby: options.row_groupby, headers: null }; + this.cols = { groupby: options.col_groupby, headers: null }; + }, + + // ---------------------------------------------------------------------- + // Configuration methods + // ---------------------------------------------------------------------- + // this.measures: list of measure [measure], measure = {field: _, string: _, type: _} + // this.rows.groupby, this.cols.groupby : list of groupbys used for describing rows (...), + // a groupby is also {field:_, string:_, type:_} + // If its type is date/datetime, field can have the corresponding interval in its description, + // for example 'create_date:week'. + set_measures: function (measures) { + this.measures = measures; + return this.update_data(); + }, + + toggle_measure: function (measure) { + var current_measure = _.findWhere(this.measures, measure); + if (current_measure) { // remove current_measure + var index = this.measures.indexOf(current_measure); + this.measures = _.without(this.measures, current_measure); + if (this.measures.length === 0) { + this.no_data = true; + } else { + _.each(this.cells, function (cell) { + cell.values.splice(index, 1); + }); + } + return $.Deferred().resolve(); + } else { // add a new measure + this.measures.push(measure); + return this.update_data(); + } + }, + + set: function (domain, row_groupby, col_groupby) { + var row_gb_changed = !_.isEqual(row_groupby, this.rows.groupby), + col_gb_changed = !_.isEqual(col_groupby, this.cols.groupby); + + this.domain = domain; + this.rows.groupby = row_groupby; + this.cols.groupby = col_groupby; + + if (row_gb_changed) { this.rows.headers = null; } + if (col_gb_changed) { this.cols.headers = null; } + + return this.update_data(); + }, + + // ---------------------------------------------------------------------- + // Cells manipulation methods + // ---------------------------------------------------------------------- + // cells are objects {x:_, y:_, values:_} where x < y and values is an array + // of values (one for each measure). The condition x < y might look + // unnecessary, but it makes the rest of the code simpler: headers + // don't krow if they are rows or cols, they just know their id, so + // it is useful that a call get_values(id1, id2) is the same as get_values(id2, id1) + add_cell : function (id1, id2, values) { + this.cells.push({x: Math.min(id1, id2), y: Math.max(id1, id2), values: values}); + }, + + get_values: function (id1, id2, default_values) { + var cells = this.cells, + x = Math.min(id1, id2), + y = Math.max(id1, id2); + for (var i = 0; i < cells.length; i++) { + if (cells[i].x == x && cells[i].y == y) { + return cells[i].values; + } + } + return (default_values || new Array(this.measures.length)); + }, + + // ---------------------------------------------------------------------- + // Headers/Rows/Cols manipulation methods + // ---------------------------------------------------------------------- + // this.rows.headers, this.cols.headers = [header] describe the tree structure + // of rows/cols. Headers are objects + // { + // id:_, (unique id obviously) + // path: [...], (array of all parents title, with its own title at the end) + // title:_, (name of the row/col) + // children:[_], (subrows or sub cols of this row/col) + // domain:_, (domain of data in this row/col) + // root:_ (ref to this.rows or this.cols corresponding to the header) + // expanded:_ (boolean, true if it has been expanded) + // } + is_row: function (id) { + return !!_.findWhere(this.rows.headers, {id:id}); + }, + + is_col: function (id) { + return !!_.findWhere(this.cols.headers, {id:id}); + }, + + get_header: function (id) { + return _.findWhere(this.rows.headers.concat(this.cols.headers), {id:id}); + }, + + _get_headers_with_depth: function (headers, depth) { + return _.filter(headers, function (header) { + return header.path.length === depth; + }); + }, + + // return all columns with a path length of 'depth' + get_cols_with_depth: function (depth) { + return this._get_headers_with_depth(this.cols.headers, depth); + }, + + // return all rows with a path length of 'depth' + get_rows_with_depth: function (depth) { + return this._get_headers_with_depth(this.rows.headers, depth); + }, + + get_ancestor_leaves: function (header) { + return _.where(this.get_ancestors_and_self(header), {expanded:false}); + }, + + // return all non expanded rows + get_rows_leaves: function () { + return _.where(this.rows.headers, {expanded:false}); + }, + + // return all non expanded cols + get_cols_leaves: function () { + return _.where(this.cols.headers, {expanded:false}); + }, + + get_ancestors: function (header) { + var self = this; + if (!header.children) return []; + return [].concat.apply([], _.map(header.children, function (c) { + return self.get_ancestors_and_self(c); + })); + }, + + get_ancestors_and_self: function (header) { + var self = this; + return [].concat.apply([header], _.map(header.children, function (c) { + return self.get_ancestors_and_self(c); + })); + }, + + get_total: function (header) { + return (header) ? this.get_values(header.id, this.get_other_root(header).headers[0].id) + : this.get_values(this.rows.headers[0].id, this.cols.headers[0].id); + }, + + get_other_root: function (header) { + return (header.root === this.rows) ? this.cols : this.rows; + }, + + main_row: function () { return this.rows.headers[0]; }, + + main_col: function () { return this.cols.headers[0]; }, + + // ---------------------------------------------------------------------- + // Table manipulation methods : fold/expand/swap + // ---------------------------------------------------------------------- + // return true if the folding changed the groupbys, false if not + fold: function (header) { + var ancestors = this.get_ancestors(header), + removed_ids = _.pluck(ancestors, 'id'); + + header.root.headers = _.difference(header.root.headers, ancestors); + header.children = []; + header.expanded = false; + + this.cells = _.reject(this.cells, function (cell) { + return (_.contains(removed_ids, cell.x) || _.contains(removed_ids, cell.y)); + }); + + var new_groupby_length = _.max(_.pluck(_.pluck(header.root.headers, 'path'), 'length')); + if (new_groupby_length < header.root.groupby.length) { + header.root.groupby.splice(new_groupby_length); + return true; + } + return false; + }, + + fold_with_depth: function (root, depth) { + var self = this; + _.each(this._get_headers_with_depth(root.headers, depth), function (header) { + self.fold(header); + }); + }, + + expand_all: function () { + this.rows.headers = null; + this.cols.headers = null; + return this.update_data(); + }, + + expand: function (header_id, groupby) { + var self = this, + header = this.get_header(header_id), + other_root = this.get_other_root(header), + this_gb = [groupby.field], + other_gbs = _.pluck(other_root.groupby, 'field'); + + if (header.path.length === header.root.groupby.length) { + header.root.groupby.push(groupby); + } + return this.perform_requests(this_gb, other_gbs, header.domain).then(function () { + var data = Array.prototype.slice.call(arguments).slice(other_gbs.length + 1); + _.each(data, function (data_pt) { + self.make_headers_and_cell( + data_pt, header.root.headers, other_root.headers, 1, header.path, true); + }); + header.expanded = true; + header.children.forEach(function (child) { + child.expanded = false; + child.root = header.root; + }); + }); + }, + + swap_axis: function () { + var temp = this.rows; + this.rows = this.cols; + this.cols = temp; + }, + + // ---------------------------------------------------------------------- + // Data updating methods + // ---------------------------------------------------------------------- + // update_data will try to preserve the expand/not expanded status of each + // column/row. If you want to expand all, then set this.cols.headers/this.rows.headers + // to null before calling update_data. + update_data: function () { + var self = this; + return this.perform_requests().then (function () { + var data = Array.prototype.slice.call(arguments); + self.no_data = !data[0].length; + if (self.no_data) { + return; + } + var row_headers = [], + col_headers = []; + self.cells = []; + + var dim_col = self.cols.groupby.length, + i, j, index; + + for (i = 0; i < self.rows.groupby.length + 1; i++) { + for (j = 0; j < dim_col + 1; j++) { + index = i*(dim_col + 1) + j; + self.make_headers_and_cell(data[index], row_headers, col_headers, i); + } + } + self.set_headers(row_headers, self.rows); + self.set_headers(col_headers, self.cols); + }); + }, + + make_headers_and_cell: function (data_pts, row_headers, col_headers, index, prefix, expand) { + var self = this; + data_pts.forEach(function (data_pt) { + var row_value = (prefix || []).concat(data_pt.attributes.value.slice(0,index)); + var col_value = data_pt.attributes.value.slice(index); + + if (expand && !_.find(col_headers, function (hdr) {return self.isEqual(col_value, hdr.path);})) { + return; + } + var row = self.find_or_create_header(row_headers, row_value, data_pt); + var col = self.find_or_create_header(col_headers, col_value, data_pt); + + var cell_value = _.map(self.measures, function (m) { + return data_pt.attributes.aggregates[m.field]; + }); + self.cells.push({ + x: Math.min(row.id, col.id), + y: Math.max(row.id, col.id), + values: cell_value + }); + }); + }, + + make_header: function (values) { + return _.extend({ + children: [], + domain: this.domain, + expanded: undefined, + id: _.uniqueId(), + path: [], + root: undefined, + title: undefined + }, values || {}); + }, + + find_or_create_header: function (headers, path, data_pt) { + var self = this; + var hdr = _.find(headers, function (header) { + return self.isEqual(path, header.path); + }); + if (hdr) { + return hdr; + } + if (!path.length) { + hdr = this.make_header({title: _t('Total')}); + headers.push(hdr); + return hdr; + } + hdr = this.make_header({ + path:path, + domain:data_pt.model._domain, + title: _t(_.last(path)) + }); + var parent = _.find(headers, function (header) { + return self.isEqual(header.path, _.initial(path, 1)); + }); + + var previous = parent.children.length ? _.last(parent.children) : parent; + headers.splice(headers.indexOf(previous) + 1, 0, hdr); + parent.children.push(hdr); + return hdr; + }, + + perform_requests: function (group1, group2, domain) { + var self = this, + requests = [], + row_gbs = _.pluck(this.rows.groupby, 'field'), + col_gbs = _.pluck(this.cols.groupby, 'field'), + field_list = row_gbs.concat(col_gbs, _.pluck(this.measures, 'field')), + fields = field_list.map(function (f) { return self.raw_field(f); }); + + group1 = group1 || row_gbs; + group2 = group2 || col_gbs; + + var i,j, groupbys; + for (i = 0; i < group1.length + 1; i++) { + for (j = 0; j < group2.length + 1; j++) { + groupbys = group1.slice(0,i).concat(group2.slice(0,j)); + requests.push(self.get_groups(groupbys, fields, domain || self.domain)); + } + } + return $.when.apply(null, requests); + }, + + // set the 'expanded' status of new_headers more or less like root.headers, with root as root + set_headers: function(new_headers, root) { + var self = this; + if (root.headers) { + _.each(root.headers, function (header) { + var corresponding_header = _.find(new_headers, function (h) { + return self.isEqual(h.path, header.path); + }); + if (corresponding_header && header.expanded) { + corresponding_header.expanded = true; + _.each(corresponding_header.children, function (c) { + c.expanded = false; + }); + } + if (corresponding_header && (!header.expanded)) { + corresponding_header.expanded = false; + corresponding_header.children = []; + } + }); + var updated_headers = _.filter(new_headers, function (header) { + return (header.expanded !== undefined); + }); + _.each(updated_headers, function (header) { + header.root = root; + }); + root.headers = updated_headers; + } else { + root.headers = new_headers; + _.each(root.headers, function (header) { + header.root = root; + header.expanded = (header.children.length > 0); + }); + } + return new_headers; + }, + + get_groups: function (groupbys, fields, domain) { + var self = this; + return this.model.query(_.without(fields, '__count')) + .filter(domain) + .lazy(false) + .group_by(groupbys) + .then(function (groups) { + return groups.filter(function (group) { + return group.attributes.length > 0; + }).map(function (group) { + var attrs = group.attributes, + grouped_on = attrs.grouped_on instanceof Array ? attrs.grouped_on : [attrs.grouped_on], + raw_grouped_on = grouped_on.map(function (f) { + return self.raw_field(f); + }); + if (grouped_on.length === 1) { + attrs.value = [attrs.value]; + } + attrs.value = _.range(grouped_on.length).map(function (i) { + if (attrs.value[i] === false) { + return _t('Undefined'); + } else if (attrs.value[i] instanceof Array) { + return attrs.value[i][1]; + } + return attrs.value[i]; + }); + attrs.aggregates.__count = group.attributes.length; + attrs.grouped_on = raw_grouped_on; + return group; + }); + }); + }, + + // if field is a fieldname, returns field, if field is field_id:interval, retuns field_id + raw_field: function (field) { + return field.split(':')[0]; + }, + + isEqual: function (path1, path2) { + if (path1.length !== path2.length) { return false; } + for (var i = 0; i < path1.length; i++) { + if (path1[i] !== path2[i]) { + return false; + } + } + return true; + }, + +}); + +})(); + + diff --git a/addons/web_graph/static/src/xml/web_graph.xml b/addons/web_graph/static/src/xml/web_graph.xml index e2e316b22d9..4522fd2504f 100644 --- a/addons/web_graph/static/src/xml/web_graph.xml +++ b/addons/web_graph/static/src/xml/web_graph.xml @@ -1,41 +1,109 @@ -<template> - <div t-name="GraphView" id="element-chart" class="oe_semantic_html_override editor-render" style="position:relative; width: 300px;"> - <a href="#" class="oe_e dropdown-menu-icon" data-toggle="dropdown" title="Graph Options">&iacute;</a> - <div class="graph_header_legend"> +<templates> + <div t-name="GraphWidget" class="graph_widget"> + <div class="graph_header"> + + <div class="btn-toolbar hidden-print" role="toolbar"> + + <div class="btn-group graph_mode_selection" data-toggle="buttons"> + <label class="btn btn-default" data-mode="pivot" title="Table Mode"> + <input type="radio" name="modes"/><span class="fa fa-table"></span> + </label> + <label class="btn btn-default" data-mode="bar" title="Bar Chart"> + <input type="radio" name="modes"/><span class="fa fa-bar-chart-o"></span> + </label> + <label class="btn btn-default" data-mode="line" title="Line Chart"> + <input type="radio" name="modes"/><span class="fa fa-check-square"></span> + </label> + <label class="btn btn-default" data-mode="pie" title="Pie Chart"> + <input type="radio" name="modes"/><span class="fa fa-adjust"></span> + </label> + </div> + + <div class="btn-group graph_heatmap" data-toggle="buttons" > + <label class="btn btn-default" data-mode="both" title="Heat Map"> + <input type="radio" name="heatmap"/><span class="fa fa-th"></span> + </label> + <label class="btn btn-default" data-mode="row" title="Heat Map (rows)"> + <input type="radio" name="heatmap"/><span class="fa fa-bars"></span> + </label> + <label class="btn btn-default" data-mode="col" title="Heat Map (columns)"> + <input type="radio" name="heatmap"/><span class="fa fa-bars fa-rotate-90"></span> + </label> </div> - <div class="graph-dropdown"> - <h3 class="menu-section"> - Graph Mode - </h3> - <ul class="graph-menu oe_graph_options"> - <li><a href="#" data-mode="pie">Pie</a></li> - <li><a href="#" data-mode="bar" data-legend="top" data-stacked="true">Bars</a> - </li> - <li><a href="#" data-mode="line">Lines</a></li> - <li><a href="#" data-mode="area" data-legend="top" data-stacked="true">Areas</a> - </li> - <li><a href="#" data-mode="radar" data-legend="inside" - data-orientation="0">Radar</a></li> - </ul> - <h3 class="menu-section"> - Legend - </h3> - <ul class="graph-menu oe_graph_options"> - <li><a href="#" data-legend="no">Hidden</a></li> - <li><a href="#" data-legend="inside">Inside</a></li> - <li><a href="#" data-legend="top">Top</a></li> - </ul> - <h3 class="menu-section"> - Actions - </h3> - <ul class="graph-menu"> - <li><a href="#" id="graph_switch">Switch Axis</a></li> - <li><a href="#" id="graph_show_data">Show Data</a></li> - <li><a href="#" id="graph_download">Download as PNG</a></li> - </ul> + + <div class="btn-group graph_options_selection"> + <label class="btn btn-default" data-choice="swap_axis" title="Swap Axis"> + <span class="fa fa-expand"></span> + </label> + <label class="btn btn-default" data-choice="update_values" title="Reload Data"> + <span class="fa fa-refresh"></span> + </label> + <label class="btn btn-default" data-choice="export_data" title="Export Data" style="display:none"> + <span class="fa fa-download"></span> + </label> </div> - <div id="editor-render-body"> + <div class="btn-group"> + <label class="btn btn-default dropdown-toggle" data-toggle="dropdown"> + Measures <span class="caret"></span> + </label> + <ul class="dropdown-menu graph_measure_selection" role="menu"> + </ul> </div> </div> + </div> + <div class="graph_main_content"></div> + </div> -</template> + <div t-name="field_selection" class="btn-group"> + <span class="btn btn-default field-selection dropdown-toggle" data-toggle="dropdown"></span> + <ul class="dropdown-menu" role="menu"> + <t t-foreach="fields" t-as="field"> + <t t-if="(field.type === 'date') || (field.type === 'datetime')"> + <li> + <a class="field-selection" t-att-data-field-id="field.id" t-att-data-interval="'day'" t-att-data-id="header_id" href="#"> + <t t-esc="field.value + ' (day)'"/> + </a> + </li> + <li> + <a class="field-selection" t-att-data-field-id="field.id" t-att-data-interval="'week'" t-att-data-id="header_id" href="#"> + <t t-esc="field.value + ' (week)'"/> + </a> + </li> + <li> + <a class="field-selection" t-att-data-field-id="field.id" t-att-data-interval="'month'" t-att-data-id="header_id" href="#"> + <t t-esc="field.value + ' (month)'"/> + </a> + </li> + <li> + <a class="field-selection" t-att-data-field-id="field.id" t-att-data-interval="'quarter'" t-att-data-id="header_id" href="#"> + <t t-esc="field.value + ' (quarter)'"/> + </a> + </li> + <li> + <a class="field-selection" t-att-data-field-id="field.id" t-att-data-interval="'year'" t-att-data-id="header_id" href="#"> + <t t-esc="field.value + ' (year)'"/> + </a> + </li> + </t> + + <t t-if="(field.type !== 'date') && (field.type !== 'datetime')"> + <li> + <a class="field-selection" t-att-data-field-id="field.id" t-att-data-id="header_id" href="#"> + <t t-esc="field.value"/> + </a> + </li> + </t> + </t> + </ul> + </div> + + <div t-name="graph_no_data" class="oe_view_nocontent" > + <div class="oe_empty_custom_dashboard"> + <p><b>No data to display.</b></p> + <p> + No data available for this graph. Try to add some records, or make sure + that there is at least one measure and no active filter in the search bar. + </p> + </div> + </div> +</templates> |
