Panoramix v1 dashboards is up
This commit is contained in:
parent
521b000ab6
commit
f6753afa75
|
|
@ -196,7 +196,7 @@ class Highchart(BaseHighchart):
|
|||
|
||||
|
||||
class HighchartBubble(BaseHighchart):
|
||||
def __init__(self, df, target_div='chart', height=800):
|
||||
def __init__(self, df, target_div=None, height=None):
|
||||
self.df = df
|
||||
self.chart = {
|
||||
'chart': {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ from pydruid import client
|
|||
from pydruid.utils.filters import Dimension, Filter
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, ForeignKey, Text, Boolean, DateTime)
|
||||
from panoramix.utils import JSONEncodedDict
|
||||
from sqlalchemy import Table as sqlaTable
|
||||
from sqlalchemy import create_engine, MetaData, desc, select, and_, Table
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import table, literal_column, text
|
||||
from flask import request
|
||||
|
||||
from copy import deepcopy, copy
|
||||
from collections import namedtuple
|
||||
|
|
@ -22,8 +22,8 @@ import sqlparse
|
|||
import requests
|
||||
import textwrap
|
||||
|
||||
from panoramix import db, get_session
|
||||
import config
|
||||
from panoramix import db, get_session, config, utils
|
||||
from panoramix.viz import viz_types
|
||||
|
||||
QueryResult = namedtuple('namedtuple', ['df', 'query', 'duration'])
|
||||
|
||||
|
|
@ -52,18 +52,32 @@ class Slice(Model, AuditMixin):
|
|||
def datasource(self):
|
||||
return self.table or self.druid_datasource
|
||||
|
||||
@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 datasource_id(self):
|
||||
datasource = self.datasource
|
||||
return datasource.id if datasource else None
|
||||
|
||||
@property
|
||||
def slice_link(self):
|
||||
def slice_url(self):
|
||||
d = json.loads(self.params)
|
||||
kwargs = "&".join([k + '=' + v for k, v in d.iteritems()])
|
||||
url = (
|
||||
from werkzeug.urls import Href
|
||||
href = Href(
|
||||
"/panoramix/{self.datasource_type}/"
|
||||
"{self.datasource_id}/?{kwargs}").format(**locals())
|
||||
"{self.datasource_id}/".format(self=self))
|
||||
return href(d)
|
||||
|
||||
@property
|
||||
def slice_link(self):
|
||||
url = self.slice_url
|
||||
return '<a href="{url}">{self.slice_name}</a>'.format(**locals())
|
||||
|
||||
@property
|
||||
|
|
@ -92,6 +106,7 @@ class Dashboard(Model, AuditMixin):
|
|||
__tablename__ = 'dashboards'
|
||||
id = Column(Integer, primary_key=True)
|
||||
dashboard_title = Column(String(500))
|
||||
position_json = Column(Text)
|
||||
slices = relationship(
|
||||
'Slice', secondary=dashboard_slices, backref='dashboards')
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,30 @@
|
|||
{% endfor %}
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename="jquery.gridster.min.css") }}">
|
||||
<style>
|
||||
a i{
|
||||
cursor: pointer;
|
||||
}
|
||||
i.drag{
|
||||
cursor: move; !important
|
||||
}
|
||||
.gridster .preview-holder {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
background-color: #AAA;
|
||||
border-color: #AAA;
|
||||
opacity: 0.3;
|
||||
}
|
||||
.gridster li {
|
||||
list-style-type: none;
|
||||
border: 1px solid gray;
|
||||
overflow: auto;
|
||||
box-shadow: 2px 2px 2px #AAA;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
}
|
||||
.gridster .dragging,
|
||||
.gridster .resizing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
img.loading {
|
||||
width: 20px;
|
||||
|
|
@ -23,6 +41,9 @@
|
|||
}
|
||||
.slice_title {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
padding: 5px;
|
||||
}
|
||||
div.gridster {
|
||||
visibility: hidden
|
||||
|
|
@ -35,21 +56,52 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block content_fluid %}
|
||||
<div class="title"><h2>{{ dashboard.dashboard_title }}</h2></div>
|
||||
<div class="title">
|
||||
<div class="row">
|
||||
<div class="col-md-1 text-left"></div>
|
||||
<div class="col-md-10 text-middle">
|
||||
<h2>
|
||||
{{ dashboard.dashboard_title }}
|
||||
<a id="savedash"><i class="fa fa-save"></i></a>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-md-1 text-right">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gridster content_fluid">
|
||||
<ul>
|
||||
{% for slice in dashboard.slices %}
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
data-row="1"
|
||||
data-col="{{ loop.index }}"
|
||||
data-sizex="2"
|
||||
data-sizey="2">
|
||||
<div class="slice_title">
|
||||
<h5>{{ slice.slice_name }}</h5>
|
||||
</div>
|
||||
<div id="slice_content_{{ slice.id }}"><img src="/static/loading.gif" class="loading"></div>
|
||||
</li>
|
||||
{% set pos = pos_dict.get(slice.id, {}) %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
<li
|
||||
id="slice_{{ slice.id }}"
|
||||
slice_id="{{ slice.id }}"
|
||||
data-row="{{ pos.row or 1 }}"
|
||||
data-col="{{ pos.col or loop.index }}"
|
||||
data-sizex="{{ pos.size_x or 4 }}"
|
||||
data-sizey="{{ pos.size_y or 4 }}">
|
||||
<div class="slice_title" style="height: 0px;">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-left">
|
||||
<a>
|
||||
<i class="fa fa-arrows drag"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-4 text-middle">
|
||||
<span>{{ slice.slice_name }}</span>
|
||||
</div>
|
||||
<div class="col-md-4 text-right" style="position: relative;">
|
||||
<a href="{{ slice.slice_url }}"><i class="fa fa-play"></i></a>
|
||||
<a class="refresh"><i class="fa fa-refresh"></i></a>
|
||||
<a><i class="fa fa-gear"></i></a>
|
||||
<a class="closewidget"><i class="fa fa-close"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ viz_macros.viz_html(viz) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -66,32 +118,49 @@
|
|||
f = d3.format(".4s");
|
||||
</script>
|
||||
<script>
|
||||
|
||||
$( document ).ready(function() {
|
||||
$(".gridster ul").gridster({
|
||||
widget_margins: [5, 5],
|
||||
widget_base_dimensions: [150, 150],
|
||||
resize: {enabled: true}
|
||||
});
|
||||
var url = "/panoramix/table/2/?flt_col_0=gender&rolling_periods=&datasource_id=2&flt_op_0=in&slice_name=Super%20Slice&viz_type=line&since=50%20years%20ago&groupby=name&metrics=total&limit=25&flt_eq_0=&granularity=one%20day&datasource_name=baby_names&where=&until=now&rolling_type=mean&datasource_type=table&skip_libs=true&standalone=true";
|
||||
$.ajax({
|
||||
url: url,
|
||||
success: function(result){
|
||||
$("#slice_content_2").html(result);
|
||||
},
|
||||
async: true,
|
||||
});
|
||||
var url = "/panoramix/table/2/?flt_col_0=gender&datasource_id=2&flt_op_0=in&viz_type=pie&since=50%20years%20ago&until=now&metrics=total&limit=10&granularity=one%20day&datasource_name=baby_names&slice_name=Pie&where=&groupby=name&flt_eq_0=&datasource_type=table&skip_libs=true&standalone=true";
|
||||
$.ajax({
|
||||
url: url,
|
||||
success: function(result){
|
||||
$("#slice_content_3").html(result);
|
||||
},
|
||||
async: true,
|
||||
});
|
||||
$("div.gridster").css('visibility', 'visible');
|
||||
});
|
||||
$( document ).ready(function() {
|
||||
var gridster = $(".gridster ul").gridster({
|
||||
widget_margins: [5, 5],
|
||||
widget_base_dimensions: [100, 100],
|
||||
draggable: {
|
||||
handle: '.drag',
|
||||
},
|
||||
resize: {
|
||||
enabled: true,
|
||||
stop: function(e, ui, $widget) {
|
||||
$widget.find("a.refresh").click();
|
||||
}
|
||||
},
|
||||
serialize_params:function($w, wgd) {
|
||||
return {
|
||||
slice_id: $($w).attr('slice_id'),
|
||||
col: wgd.col,
|
||||
row: wgd.row,
|
||||
size_x: wgd.size_x,
|
||||
size_y: wgd.size_y
|
||||
};
|
||||
},
|
||||
}).data('gridster');
|
||||
$("div.gridster").css('visibility', 'visible');
|
||||
$("#savedash").click(function(){
|
||||
var data = gridster.serialize();
|
||||
console.log(data);
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: '/panoramix/save_dash/{{ dashboard.id }}/',
|
||||
data: {data: JSON.stringify(data)},
|
||||
success: function(){console.log('Sucess!')},
|
||||
});
|
||||
});
|
||||
$("a.closewidget").click(function(){
|
||||
var li = $(this).parents("li");
|
||||
gridster.remove_widget(li);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% for slice in dashboard.slices %}
|
||||
{% set viz = slice.viz %}
|
||||
{% import viz.template as viz_macros %}
|
||||
{{ viz_macros.viz_js(viz) }}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
{% macro viz_html(viz) %}
|
||||
<div id="{{ viz.token }}" style="height:100%; width:100%;">
|
||||
<div id="{{ viz.token }}" style="height:100%; width: 100%">
|
||||
<img src="{{ url_for("static", filename="loading.gif") }}" class="loading">
|
||||
<div class="chart" style="height:100%; width: 100%"></div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
|
|
@ -16,19 +17,29 @@
|
|||
useUTC: false
|
||||
},
|
||||
});
|
||||
$("#viz_type").click(function(){
|
||||
$("#queryform").submit();
|
||||
});
|
||||
var url = window.location + "&json=true";
|
||||
console.log(url);
|
||||
$.getJSON(url, function(data){
|
||||
console.log(data);
|
||||
$("#{{ viz.token }}").highcharts('{{ viz.chart_call }}', data);
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
|
||||
$("#{{ viz.token }}").html(err);
|
||||
});
|
||||
var token = $("#{{ viz.token }}");
|
||||
var loading = $("#{{ viz.token }}").find("img.loading");
|
||||
var chart = $("#{{ viz.token }}").find("div.chart");
|
||||
var refresh = function(){
|
||||
chart.hide();
|
||||
loading.show();
|
||||
var url = "{{ viz.get_url(json="true")|safe }}";
|
||||
$.getJSON(url, function(data){
|
||||
chart.width(token.width());
|
||||
chart.height(token.height()-40);
|
||||
chart.highcharts('{{ viz.chart_call }}', data);
|
||||
chart.show();
|
||||
token.find("img.loading").hide();
|
||||
})
|
||||
.fail(function(xhr) {
|
||||
var err = '<div class="alert alert-danger">' + xhr.responseText + '</div>';
|
||||
loading.hide();
|
||||
chart.show();
|
||||
chart.html(err);
|
||||
});
|
||||
};
|
||||
refresh();
|
||||
token.parent().find("a.refresh").click(refresh);
|
||||
});
|
||||
</script>
|
||||
{% endmacro %}
|
||||
|
|
|
|||
|
|
@ -35,15 +35,16 @@
|
|||
{% macro viz_js(viz) %}
|
||||
{% if viz.args.get("async") != "true" %}
|
||||
<script>
|
||||
var url = window.location + '&async=true&standalone=true&skip_libs=true';
|
||||
var url = "{{ viz.get_url(async="true", standalone="true", skip_libs="true")|safe }}";
|
||||
console.log(url);
|
||||
$("#{{ viz.token }}").load(url, function(){
|
||||
var table = $('table').DataTable({
|
||||
paging: false,
|
||||
})
|
||||
searching: false,
|
||||
});
|
||||
$("#{{ viz.token }}").show();
|
||||
table.column('-1').order( 'desc' ).draw();
|
||||
$("img.loading").hide();
|
||||
$("#{{ viz.token }} img.loading").hide();
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,34 @@ from datetime import datetime
|
|||
from sqlalchemy.types import TypeDecorator, TEXT
|
||||
import json
|
||||
import parsedatetime
|
||||
import functools
|
||||
|
||||
|
||||
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 parse_human_datetime(s):
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ appbuilder.add_view(
|
|||
class DashboardModelView(ModelView, DeleteMixin):
|
||||
datamodel = SQLAInterface(models.Dashboard)
|
||||
list_columns = ['dashboard_link', 'created_by']
|
||||
edit_columns = ['dashboard_title', 'slices',]
|
||||
edit_columns = ['dashboard_title', 'slices', 'position_json']
|
||||
add_columns = edit_columns
|
||||
|
||||
|
||||
|
|
@ -186,7 +186,7 @@ class DatasourceModelView(ModelView, DeleteMixin):
|
|||
appbuilder.add_view(
|
||||
DatasourceModelView,
|
||||
"Druid Datasources",
|
||||
icon="fa-cube")
|
||||
icon="fa-cubes")
|
||||
|
||||
|
||||
@app.route('/health')
|
||||
|
|
@ -218,12 +218,13 @@ class Panoramix(BaseView):
|
|||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
table,
|
||||
form_data=request.args, view=self)
|
||||
form_data=request.args)
|
||||
if request.args.get("json") == "true":
|
||||
try:
|
||||
payload = obj.get_json()
|
||||
status=200
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
payload = str(e)
|
||||
status=500
|
||||
return Response(
|
||||
|
|
@ -233,11 +234,27 @@ class Panoramix(BaseView):
|
|||
else:
|
||||
return self.render_template("panoramix/viz.html", viz=obj)
|
||||
|
||||
@has_access
|
||||
@expose("/save_dash/<dashboard_id>/", methods=['GET', 'POST'])
|
||||
def save_dash(self, dashboard_id):
|
||||
data = json.loads(request.form.get('data'))
|
||||
slice_ids = [int(d['slice_id']) for d in data]
|
||||
print slice_ids
|
||||
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]
|
||||
print dash.slices
|
||||
dash.position_json = json.dumps(data, indent=4)
|
||||
session.merge(dash)
|
||||
session.commit()
|
||||
session.close()
|
||||
return "SUCCESS"
|
||||
|
||||
@has_access
|
||||
@expose("/datasource/<datasource_name>/")
|
||||
def datasource(self, datasource_name):
|
||||
viz_type = request.args.get("viz_type")
|
||||
|
||||
datasource = (
|
||||
db.session
|
||||
.query(models.Datasource)
|
||||
|
|
@ -250,7 +267,7 @@ class Panoramix(BaseView):
|
|||
viz_type = "table"
|
||||
obj = viz.viz_types[viz_type](
|
||||
datasource,
|
||||
form_data=request.args, view=self)
|
||||
form_data=request.args)
|
||||
if request.args.get("json"):
|
||||
return Response(
|
||||
json.dumps(obj.get_query(), indent=4),
|
||||
|
|
@ -265,8 +282,13 @@ class Panoramix(BaseView):
|
|||
@expose("/save/")
|
||||
def save(self):
|
||||
session = db.session()
|
||||
d = request.args.to_dict(flat=False)
|
||||
as_list = ('metrics', 'groupby')
|
||||
for m in as_list:
|
||||
if d[m] and not isinstance(d[m]):
|
||||
d[m] = [d[m]]
|
||||
obj = models.Slice(
|
||||
params=json.dumps(request.args.to_dict()),
|
||||
params=json.dumps(d, indent=4),
|
||||
viz_type=request.args.get('viz_type'),
|
||||
datasource_name=request.args.get('datasource_name'),
|
||||
datasource_id=request.args.get('datasource_id'),
|
||||
|
|
@ -289,8 +311,11 @@ class Panoramix(BaseView):
|
|||
.filter(models.Dashboard.id == id_)
|
||||
.first()
|
||||
)
|
||||
pos_dict = {
|
||||
int(o['slice_id']):o for o in json.loads(dashboard.position_json)}
|
||||
return self.render_template(
|
||||
"panoramix/dashboard.html", dashboard=dashboard)
|
||||
"panoramix/dashboard.html", dashboard=dashboard,
|
||||
pos_dict=pos_dict)
|
||||
|
||||
@has_access
|
||||
@expose("/refresh_datasources/")
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
from datetime import datetime
|
||||
from flask import flash, request
|
||||
import pandas as pd
|
||||
from collections import OrderedDict
|
||||
import config
|
||||
from datetime import datetime
|
||||
from urllib import urlencode
|
||||
import uuid
|
||||
import numpy as np
|
||||
|
||||
from panoramix import utils
|
||||
from flask import flash
|
||||
from werkzeug.datastructures import MultiDict
|
||||
from werkzeug.urls import Href
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from panoramix import utils, config
|
||||
from panoramix.highchart import Highchart, HighchartBubble
|
||||
from panoramix.forms import form_factory
|
||||
|
||||
|
|
@ -25,14 +28,28 @@ class BaseViz(object):
|
|||
js_files = []
|
||||
css_files = []
|
||||
|
||||
def __init__(self, datasource, form_data, view):
|
||||
self.token = form_data.get('token', 'token_' + uuid.uuid4().hex[:8])
|
||||
def __init__(self, datasource, form_data):
|
||||
self.datasource = datasource
|
||||
self.view = view
|
||||
if isinstance(form_data, MultiDict):
|
||||
self.args = form_data.to_dict(flat=True)
|
||||
else:
|
||||
self.args = form_data
|
||||
self.form_data = form_data
|
||||
self.args = form_data
|
||||
self.metrics = form_data.getlist('metrics') or ['count']
|
||||
self.groupby = form_data.getlist('groupby') or []
|
||||
self.token = self.args.get('token', 'token_' + uuid.uuid4().hex[:8])
|
||||
|
||||
as_list = ('metrics', 'groupby')
|
||||
d = self.args
|
||||
for m in as_list:
|
||||
if m in d and d[m] and not isinstance(d[m], list):
|
||||
d[m] = [d[m]]
|
||||
self.metrics = self.args.get('metrics') or ['count']
|
||||
self.groupby = self.args.get('groupby') or []
|
||||
|
||||
def get_url(self, **kwargs):
|
||||
d = self.args.copy()
|
||||
d.update(kwargs)
|
||||
href = Href('/panoramix/table/2/')
|
||||
return href(d)
|
||||
|
||||
def get_df(self):
|
||||
self.error_msg = ""
|
||||
|
|
@ -60,7 +77,7 @@ class BaseViz(object):
|
|||
return form_factory(self)
|
||||
|
||||
def query_filters(self):
|
||||
args = self.form_data
|
||||
args = self.args
|
||||
# Building filters
|
||||
filters = []
|
||||
for i in range(1, 10):
|
||||
|
|
@ -78,9 +95,9 @@ class BaseViz(object):
|
|||
"""
|
||||
Building a query object
|
||||
"""
|
||||
args = self.form_data
|
||||
groupby = args.getlist("groupby") or []
|
||||
metrics = args.getlist("metrics") or ['count']
|
||||
args = self.args
|
||||
groupby = args.get("groupby") or []
|
||||
metrics = args.get("metrics") or ['count']
|
||||
granularity = args.get("granularity", "1 day")
|
||||
if granularity != "all":
|
||||
granularity = utils.parse_human_timedelta(
|
||||
|
|
@ -162,17 +179,18 @@ class BubbleViz(HighchartsViz):
|
|||
js_files = ['highstock.js', 'highcharts-more.js']
|
||||
|
||||
def query_obj(self):
|
||||
args = self.form_data
|
||||
d = super(BubbleViz, self).query_obj()
|
||||
d['granularity'] = 'all'
|
||||
d['groupby'] = list({
|
||||
request.args.get('series'),
|
||||
request.args.get('entity')
|
||||
})
|
||||
self.x_metric = request.args.get('x')
|
||||
self.y_metric = request.args.get('y')
|
||||
self.z_metric = request.args.get('size')
|
||||
self.entity = request.args.get('entity')
|
||||
self.series = request.args.get('series')
|
||||
args.get('series'),
|
||||
args.get('entity')
|
||||
})
|
||||
self.x_metric = args.get('x')
|
||||
self.y_metric = args.get('y')
|
||||
self.z_metric = args.get('size')
|
||||
self.entity = args.get('entity')
|
||||
self.series = args.get('series')
|
||||
d['metrics'] = [
|
||||
self.z_metric,
|
||||
self.x_metric,
|
||||
|
|
@ -213,6 +231,7 @@ class TimeSeriesViz(HighchartsViz):
|
|||
]
|
||||
|
||||
def get_df(self):
|
||||
args = self.args
|
||||
df = super(TimeSeriesViz, self).get_df()
|
||||
metrics = self.metrics
|
||||
df = df.pivot_table(
|
||||
|
|
@ -220,8 +239,8 @@ class TimeSeriesViz(HighchartsViz):
|
|||
columns=self.groupby,
|
||||
values=metrics,)
|
||||
|
||||
rolling_periods = request.args.get("rolling_periods")
|
||||
rolling_type = request.args.get("rolling_type")
|
||||
rolling_periods = args.get("rolling_periods")
|
||||
rolling_type = args.get("rolling_type")
|
||||
if rolling_periods and rolling_type:
|
||||
if rolling_type == 'mean':
|
||||
df = pd.rolling_mean(df, int(rolling_periods))
|
||||
|
|
|
|||
Loading…
Reference in New Issue