Permalink
Browse files

Refactored api handlers.

Added charts sharing!
Updated frontend.
Updated LICENSE.
  • Loading branch information...
1 parent c52e27b commit bab21b811752bcfa6d2b6cc60eff9a944e7d1925 @Lispython Lispython committed Apr 6, 2013
View
9 LICENSE
@@ -1,7 +1,5 @@
-Copyright (c) 2012 by GottWall team, see AUTHORS for more details and contributors.
-See AUTHORS for more details.
-
-Some rights reserved.
+Copyright (c) 2012 by Alexandr Sokolovskiy and and individual contributors.
+All rights reserved.
Redistribution and use in source and binary forms of the software as well
as documentation, with or without modification, are permitted provided
@@ -15,10 +13,11 @@ that the following conditions are met:
disclaimer in the documentation and/or other materials provided
with the distribution.
-* The names of the contributors may not be used to endorse or
+* The names of the GottWall contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
+
THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
View
15 README.rst
@@ -1,7 +1,7 @@
-Welcome to gottwall's documentation!
+Welcome to GottWall's documentation!
======================================
-GottWall is a scalable realtime metrics aggragation platform.
+GottWall is a scalable realtime metrics collecting and aggregation platform and service.
This package, at its core, is just a simple aggregation server and
beautiful customizable web dashboard for visualizing metrics with charts.
@@ -17,6 +17,8 @@ Features
- Beautiful customizable dashboard for visualizing metrics with charts.
- Data aggregation
+- Data collection
+- Embedded charts (`HTML <http://demo.gottwall.com/api/embedded/hash.html>`_, iframe, `javascript <http://demo.gottwall.com/api/mbedded/hash.js>`, `JSON <http://demo.gottwall.com/api/embedded/hash.json>`_)
Screenshots
-----------
@@ -80,6 +82,7 @@ The following transport available:
- Redis transport backend
- TCP/IP transport backend
+- TODO: UDP transport backend
- HTTP transport backend
@@ -93,8 +96,6 @@ The following clients are officially recognized as production-ready, and support
- stati-redis (`stati-redis-python <http://github.com/GottWall/stati-redis-python>`_) with redis transport.
-
-
CONTRIBUTE
----------
@@ -107,3 +108,9 @@ We need you help.
#. Send a pull request and bug the maintainer until it gets merged and published.
.. _`the repository`: https://github.com/GottWall/GottWall/
+
+
+ETC
+---
+
+* Graphs widgets rendered with `rickshaw <http://code.shutterstock.com/rickshaw/>`_ (HTML5 + SVG and `d3.js <http://d3js.org/>`_) library.
View
4 gottwall/__init__.py
@@ -4,7 +4,7 @@
gottwall
~~~~~~~~
-GottWall is a scalable realtime metrics aggragation platform.
+GottWall is a scalable realtime metrics collecting and aggregation platform and service.
:copyright: (c) 2012 - 2013 by GottWall team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
@@ -27,6 +27,8 @@
__version_info__ = __version__.split('.')
__build__ = 0x000034
+GOTTWALL_HOME = "http://demo.gottwall.com"
+GOTTWALL_DESCRIPTION = "GottWall is a scalable realtime metrics aggrigation platform."
def get_version():
return __version__
View
317 gottwall/api_v1.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""
+api_v1
+~~~~~~
+
+API handler for v1
+
+:copyright: (c) 2012 - 2013 by GottWall team, see AUTHORS for more details.
+:license: BSD, see LICENSE for more details.
+:github: http://github.com/GottWall/GottWall
+"""
+import logging
+from datetime import datetime
+
+from dateutil.relativedelta import relativedelta
+import tornado.escape
+from tornado.web import authenticated
+
+from tornado.escape import json_decode
+import tornado.web
+import tornado.gen
+from tornado import gen
+
+from gottwall.utils import (timestamp_to_datetime, date_range, format_date_by_period,
+ date_min, date_max, datetime_to_utimestamp, get_datetime)
+from gottwall.settings import DATE_FILTER_FORMAT, PERIODS, DEFAULT_EMBEDDED_PARAMS
+
+logger = logging.getLogger('gottwall.apiv1')
+
+from handlers import SERVER_NAME, BaseHandler, JSONMixin, APIHandler
+
+
+class TimeMixin(object):
+ def convert_date_range(self, from_date, to_date):
+ """Convert str from_date and to_data objects to
+ datetime object
+
+ :param from_date: from date string
+ :param to_date: to date string
+ :return: tuple (from_date, to_date)
+ """
+
+ from_date = timestamp_to_datetime(from_date, DATE_FILTER_FORMAT) if from_date else from_date
+ to_date = timestamp_to_datetime(to_date, DATE_FILTER_FORMAT) if to_date else to_date
+ return from_date, to_date
+
+ def clean_date_range(self, from_date, to_date):
+ try:
+ from_date, to_date = self.convert_date_range(from_date, to_date)
+ if from_date > to_date:
+ raise RuntimeError
+ except (ValueError, RuntimeError):
+ self.set_status(400)
+ self.json_response({"text": "Invalid date range"})
+ return None, None
+ return from_date, to_date
+
+
+
+class StatsMixin(TimeMixin):
+
+ def get_params(self):
+ name = self.get_argument('name', None)
+ from_date = self.get_argument('from_date', None)
+ to_date = self.get_argument('to_date', None)
+ period = self.get_argument('period', 'month')
+ filter_name = self.get_argument('filter_name', None)
+ filter_value = self.get_argument('filter_value', None)
+
+ return name, from_date, to_date, period, filter_name, filter_value
+
+ def validate_name(self, name, period):
+ if not all([name, period]):
+ self.set_status(400)
+ self.json_response({"text": "You need specify name and period"})
+ return
+ return True
+
+
+class StatsHandlerV1(APIHandler, StatsMixin):
+ """Load periods statistics
+ """
+
+ @authenticated
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self, project, *args, **kwargs):
+
+ try:
+ name, from_date, to_date, period, filter_name, filter_value = self.get_params()
+ except Exception:
+ self.set_status(400)
+ self.json_response({"text": "Bad request"})
+ return
+
+ from_date, to_date = self.clean_date_range(from_date, to_date)
+
+ if self.validate_name(name, period) and from_date and to_date:
+
+ data = yield gen.Task(self.application.storage.slice_data,
+ project, name, period, from_date, to_date, filter_name, filter_value)
+
+ self.json_response({"range": list(data),
+ "project": project,
+ "period": period,
+ "name": name,
+ "filter_name": filter_name,
+ "filter_value": filter_value,
+ "avg": 0})
+
+
+class StatsDataSetHandlerV1(APIHandler, StatsMixin):
+ """Load data for filters without filter value
+ """
+ @authenticated
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self, project, *args, **kwargs):
+
+ try:
+ name, from_date, to_date, period, filter_name, filter_value = self.get_params()
+ from_date, to_date = self.convert_date_range(from_date, to_date)
+
+ except Exception:
+ self.set_status(400)
+ self.json_response({"text": "Bad request"})
+ return
+
+ from_date, to_date = self.clean_date_range(from_date, to_date)
+
+ if self.validate_name(name, period) and from_date and to_date:
+
+ data = yield gen.Task(self.application.storage.slice_data_set,
+ project, name, period, from_date, to_date, filter_name)
+
+ self.json_response({"data": data,
+ "project": project,
+ "period": period,
+ "name": name,
+ "filter_name": filter_name,
+ "date_range": [format_date_by_period(x, period)
+ for x in date_range(date_min(from_date, period),
+ date_max(to_date, period), period)]})
+
+
+class MetricsHandlerV1(APIHandler):
+ """Load metrics structure
+ """
+ @authenticated
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self, project, *args, **kwargs):
+ metrics = yield gen.Task(self.application.storage.metrics, project)
+ self.json_response(metrics)
+
+
+class EmbeddedCreateHandlerV1(APIHandler, TimeMixin):
+ """Create embedded charts
+ """
+
+ def validate_period(self, period):
+ if period not in PERIODS:
+ self.set_status(400)
+ self.json_response({"text": "You need specify name and period"})
+ return
+ return True
+
+ @authenticated
+ @tornado.web.asynchronous
+ @gen.engine
+ def post(self, project, *args, **kwargs):
+
+ try:
+ data = json_decode(self.request.body)
+ metrics = data['metrics']
+ period = data['period']
+ except Exception:
+ self.set_status(400)
+ self.json_response({"text": "Bad request"})
+ return
+
+ if not self.validate_period(period):
+ return
+
+ embedded_hash = (yield gen.Task(
+ self.application.storage.make_embedded,
+ project, period, metrics))
+
+ if not embedded_hash:
+ self.set_status(500)
+ self.json_response({"text": "Server error"})
+ return
+
+ link_template = "{0}://{1}".format(self.request.protocol, self.request.host)
+
+ response_data = {
+ "html_link": link_template + self.reverse_url('api-v1-html-embedded', embedded_hash),
+ "js_link": link_template + self.reverse_url('api-v1-js-embedded', embedded_hash),
+ "json_link": link_template + self.reverse_url('api-v1-json-embedded', embedded_hash)}
+
+ response_data["iframe"] = '<iframe src="{0}"></iframe>'.format(response_data['html_link'])
+ self.json_response(response_data)
+
+
+class EmbeddedBaseHandlerV1(BaseHandler, TimeMixin, JSONMixin):
+ def get_params(self):
+ from_date = self.get_argument('from_date', datetime.now() - relativedelta(months=1))
+ to_date = self.get_argument('to_date', datetime.now())
+ period = self.get_argument('period', 'month')
+
+ return from_date, to_date, period
+
+ @gen.engine
+ def get_data(self, uid, callback=None):
+ from_date, to_date, period = self.get_params()
+ from_date, to_date = self.clean_date_range(from_date, to_date)
+
+ if not any([from_date, to_date]):
+ return
+
+ meta_info = (yield gen.Task(self.application.storage.get_embedded, uid))
+
+ response_data = {
+ "metrics": [],
+ "period": self.get_argument('period', meta_info['period']),
+ "from_date": from_date.strftime(DATE_FILTER_FORMAT),
+ "to_date": to_date.strftime(DATE_FILTER_FORMAT)}
+
+ names = []
+
+ for metric in meta_info['metrics']:
+ metric['range'] = (yield gen.Task(
+ self.application.storage.slice_data,
+ meta_info['project'], metric['m'], response_data['period'],
+ from_date, to_date, metric.get('fn'), metric.get('fv')))
+
+ metric['name'] = metric.pop('m')
+ names.append(metric['name'])
+ metric['filter_name'] = metric.pop('fn', None)
+ metric['filter_value'] = metric.pop('fv', None)
+
+ response_data['metrics'] = meta_info['metrics']
+ response_data['name'] = meta_info.get('name',
+ self.get_argument(
+ 'name', ' | '.join(names)))
+ response_data['generator'] = SERVER_NAME
+
+
+ if callback:
+ callback(response_data)
+
+
+class HTMLEmbeddedHandlerV1(EmbeddedBaseHandlerV1):
+
+ def get_chart_params(self):
+ EMBEDDED_PARAMS = self.application.config.get('EMBEDDED_PARAMS', {})
+
+ height = int(self.get_argument('height', EMBEDDED_PARAMS.get('height')) or
+ DEFAULT_EMBEDDED_PARAMS['height'])
+ width = int(self.get_argument('width', EMBEDDED_PARAMS.get('width')) or
+ DEFAULT_EMBEDDED_PARAMS['width'])
+ renderer = self.get_argument('renderer', EMBEDDED_PARAMS.get('renderer')) or\
+ DEFAULT_EMBEDDED_PARAMS['renderer']
+ interpolation = self.get_argument('interpolation', EMBEDDED_PARAMS.get('interpolation')) or\
+ DEFAULT_EMBEDDED_PARAMS['interpolation']
+
+ return height, width, renderer, interpolation
+
+
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self, uid, *args, **kwargs):
+ response_data = (yield gen.Task(self.get_data, uid))
+ height, width, renderer, interpolation = self.get_chart_params()
+
+ def x_converter(x):
+ return datetime_to_utimestamp(get_datetime(x, response_data['period']))
+
+ self.render("embedded.html", config=self.application.config,
+ data=response_data, width=width, height=height,
+ renderer=renderer, interpolation=interpolation,
+ x_converter=x_converter, uid=uid)
+
+
+class JSEmbeddedHandlerV1(HTMLEmbeddedHandlerV1):
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self, uid, *args,**kwargs):
+ from_date, to_date, period = self.get_params()
+ from_date, to_date = self.clean_date_range(from_date, to_date)
+
+ if not any([from_date, to_date]):
+ return
+
+ height, width, renderer, interpolation = self.get_chart_params()
+
+
+ self.render("js_embedded.html", config=self.application.config,
+ width=width, height=height,
+ from_date=from_date.strftime(DATE_FILTER_FORMAT),
+ to_date=to_date.strftime(DATE_FILTER_FORMAT), period=period,
+ renderer=renderer, interpolation=interpolation, uid=uid)
+
+
+
+class JSONEmbeddedHandlerV1(EmbeddedBaseHandlerV1):
+
+ @tornado.web.asynchronous
+ @gen.engine
+ def get(self, uid, *args, **kwargs):
+
+ response_data = (yield gen.Task(
+ self.get_data, uid))
+
+ self.json_response(response_data)
View
26 gottwall/app.py
@@ -18,8 +18,10 @@
from tornado.web import Application, URLSpec
from handlers import DashboardHandler, LoginHandler, HomeHandler,\
- StatsHandler, MetricsHandler, LogoutHandler, StatsDataSetHandler, \
- NotFoundHandler
+ NotFoundHandler, LogoutHandler
+from api_v1 import (StatsHandlerV1, MetricsHandlerV1, StatsDataSetHandlerV1,
+ HTMLEmbeddedHandlerV1, JSONEmbeddedHandlerV1,
+ JSEmbeddedHandlerV1, EmbeddedCreateHandlerV1)
from jinja_utils import load_filters, load_globals
from processing import Tasks
@@ -47,12 +49,20 @@ def __init__(self, config):
(r"{0}/login".format(self.config['PREFIX']), LoginHandler, params, 'login'),
(r"{0}/logout".format(self.config['PREFIX']), LogoutHandler, params, 'logout'),
(r"{0}/dashboard".format(self.config['PREFIX']), DashboardHandler, params, 'dashboard'),
- (r"{0}/(?P<project>.+)/api/stats".format(self.config['PREFIX']),
- StatsHandler, params, 'api-stats'),
- (r"{0}/(?P<project>.+)/api/stats_dataset".format(self.config['PREFIX']),
- StatsDataSetHandler, params, 'api-stats-dataset'),
- (r"{0}/(?P<project>.+)/api/metrics".format(self.config['PREFIX']),
- MetricsHandler, params, 'api-metrics'),
+ (r"{0}/api/v1/(?P<project>.+)/stats".format(self.config['PREFIX']),
+ StatsHandlerV1, params, 'api-v1-stats'),
+ (r"{0}/api/v1/(?P<project>.+)/stats_dataset".format(self.config['PREFIX']),
+ StatsDataSetHandlerV1, params, 'api-v1-stats-dataset'),
+ (r"{0}/api/v1/(?P<project>.+)/metrics".format(self.config['PREFIX']),
+ MetricsHandlerV1, params, 'api-v1-metrics'),
+ (r"{0}/api/v1/(?P<project>.+)/embedded/".format(self.config['PREFIX']),
+ EmbeddedCreateHandlerV1, params, 'api-v1-embedded-create'),
+ (r"{0}/api/v1/embedded/(?P<uid>.+).html".format(self.config['PREFIX']),
+ HTMLEmbeddedHandlerV1, params, 'api-v1-html-embedded'),
+ (r"{0}/api/v1/embedded/(?P<uid>.+).json".format(self.config['PREFIX']),
+ JSONEmbeddedHandlerV1, params, 'api-v1-json-embedded'),
+ (r"{0}/api/v1/embedded/(?P<uid>.+).js".format(self.config['PREFIX']),
+ JSEmbeddedHandlerV1, params, 'api-v1-js-embedded'),
(r"{0}/".format(self.config['PREFIX']), HomeHandler, params, 'home'),
(r"{0}.*".format(self.config['PREFIX']), NotFoundHandler, params, 'not_found'),
]
View
10 gottwall/default_config.py
@@ -32,7 +32,7 @@
login_url = '/login'
-site_title=u"GottWall - statistics aggregator"
+site_title=u"GottWall - metrics aggregation platform"
ALEMBIC_SCRIPT_LOCATION = 'gottwall:migrations'
@@ -59,3 +59,11 @@
'jinja2.ext.do',
'jinja2.ext.i18n'
)
+
+
+EMBEDDED_PARAMS = {
+ "height": 400,
+ "width": 800,
+ "renderer": "line", # area, stack, bar, line, and scatterplot,
+ "interpolation": 'linear' # linear, step-after, cardinal, basis
+ }
View
155 gottwall/handlers.py
@@ -5,13 +5,15 @@
handlers
~~~~~~~~
-module description
-:copyright: (c) 2012 by GottWall team, see AUTHORS for more details.
+:copyright: (c) 2012 - 2013 by GottWall team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
:github: http://github.com/GottWall/GottWall
"""
import logging
+from datetime import datetime
+
+from dateutil.relativedelta import relativedelta
import tornado.escape
from tornado.auth import GoogleMixin
from tornado.web import RequestHandler, HTTPError, asynchronous, authenticated
@@ -25,13 +27,14 @@
import tornado.gen
from tornado import gen
-from gottwall import get_version
+from gottwall import get_version, GOTTWALL_HOME, GOTTWALL_DESCRIPTION
from gottwall.utils import (timestamp_to_datetime, date_range, format_date_by_period,
date_min, date_max)
-from gottwall.settings import DATE_FILTER_FORMAT
+from gottwall.settings import DATE_FILTER_FORMAT, PERIODS
logger = logging.getLogger('gottwall')
+SERVER_NAME = "GottWall / {0}".format(get_version())
class User(object):
"""Request user object
@@ -50,7 +53,7 @@ def is_authenticated(self):
class BaseHandler(RequestHandler):
def __init__(self, *args, **kwargs):
super(BaseHandler, self).__init__(*args, **kwargs)
- self.set_header("Server", "GottWall/{0}".format(get_version()))
+ self.set_header("Server", SERVER_NAME)
def initialize(self, config, db, env):
self.config = config
@@ -86,6 +89,9 @@ def render(self, template, **kwargs):
kwargs['user'] = self.current_user
kwargs['reverse'] = self.reverse_url
kwargs['version'] = get_version()
+ kwargs['generator'] = SERVER_NAME
+ kwargs['gottwall_home'] = GOTTWALL_HOME
+ kwargs['gottwall_description'] = GOTTWALL_DESCRIPTION
data = self.render_to_string(template, context=kwargs)
return self.finish(data)
@@ -128,26 +134,15 @@ def get(self, *args, **kwargs):
self.render("home.html", config=self.application.config,
projects=self.config['PROJECTS'] if self.current_user else [])
+
class NotFoundHandler(BaseHandler):
def get(self, *args, **kwargs):
self.set_status(404)
self.render("404.html", config=self.application.config)
-class APIHandler(BaseHandler):
- """Base class for api handlers
- """
-
- def check_auth(self):
- """Check authorization
- """
- key = self.request.headers.get('Authorization')
- if key != self.application.config['SECRET_KEY']:
- logger.error("Invalid authorixation key: {0}".format(key))
- raise HTTPError(401, "Authorization required")
- return True
-
+class JSONMixin(object):
def json_response(self, data, finish=True):
output_json = tornado.escape.json_encode(data)
self.set_header("Content-Type", "application/json")
@@ -157,126 +152,20 @@ def json_response(self, data, finish=True):
return output_json
-class StatsMixin(object):
-
- def convert_date_range(self, from_date, to_date):
- """Convert str from_date and to_data objects to
- datetime object
+class APIHandler(BaseHandler, JSONMixin):
+ """Base class for api handlers
+ """
- :param from_date: from date string
- :param to_date: to date string
- :return: tuple (from_date, to_date)
+ def check_auth(self):
+ """Check authorization
"""
-
- from_date = timestamp_to_datetime(from_date, DATE_FILTER_FORMAT) if from_date else from_date
- to_date = timestamp_to_datetime(to_date, DATE_FILTER_FORMAT) if to_date else to_date
- return from_date, to_date
-
- def get_params(self):
- name = self.get_argument('name', None)
- from_date = self.get_argument('from_date', None)
- to_date = self.get_argument('to_date', None)
- period = self.get_argument('period', 'week')
- filter_name = self.get_argument('filter_name', None)
- filter_value = self.get_argument('filter_value', None)
-
- return name, from_date, to_date, period, filter_name, filter_value
-
- def clean_period(self, from_date, to_date):
- try:
- from_date, to_date = self.convert_date_range(from_date, to_date)
- except ValueError:
- self.set_status(400)
- self.json_response({"text": "Invalid date range params"})
- return None, None
- return from_date, to_date
-
-
- def validate_name(self, name, period):
- if not all([name, period]):
- self.set_status(400)
- self.json_response({"text": "You need specify name and period"})
- return
+ key = self.request.headers.get('Authorization')
+ if key != self.application.config['SECRET_KEY']:
+ logger.error("Invalid authorixation key: {0}".format(key))
+ raise HTTPError(401, "Authorization required")
return True
-class StatsHandler(APIHandler, StatsMixin):
- """Load periods statistics
- """
-
- @authenticated
- @tornado.web.asynchronous
- @gen.engine
- def get(self, project, *args, **kwargs):
-
- try:
- name, from_date, to_date, period, filter_name, filter_value = self.get_params()
- except Exception:
- self.set_status(400)
- self.json_response({"text": "Bad request"})
- return
-
- from_date, to_date = self.clean_period(from_date, to_date)
-
- if self.validate_name(name, period) and from_date and to_date:
-
- data = yield gen.Task(self.application.storage.slice_data,
- project, name, period, from_date, to_date, filter_name, filter_value)
-
- self.json_response({"range": list(data),
- "project": project,
- "period": period,
- "name": name,
- "filter_name": filter_name,
- "filter_value": filter_value,
- "avg": 0})
-
-
-class StatsDataSetHandler(APIHandler, StatsMixin):
- """Load data for filters without filter value
- """
- @authenticated
- @tornado.web.asynchronous
- @gen.engine
- def get(self, project, *args, **kwargs):
-
- try:
- name, from_date, to_date, period, filter_name, filter_value = self.get_params()
- from_date, to_date = self.convert_date_range(from_date, to_date)
-
- except Exception:
- self.set_status(400)
- self.json_response({"text": "Bad request"})
- return
-
- from_date, to_date = self.clean_period(from_date, to_date)
-
- if self.validate_name(name, period) and from_date and to_date:
-
- data = yield gen.Task(self.application.storage.slice_data_set,
- project, name, period, from_date, to_date, filter_name)
-
- self.json_response({"data": data,
- "project": project,
- "period": period,
- "name": name,
- "filter_name": filter_name,
- "date_range": [format_date_by_period(x, period)
- for x in date_range(date_min(from_date, period),
- date_max(to_date, period), period)]})
-
-
-class MetricsHandler(APIHandler):
- """Load metrics structure
- """
- @authenticated
- @tornado.web.asynchronous
- @gen.engine
- def get(self, project, *args, **kwargs):
- metrics = yield gen.Task(self.application.storage.metrics, project)
- self.json_response(metrics)
-
-
class LogoutHandler(BaseHandler):
def get(self):
View
20 gottwall/settings.py
@@ -43,22 +43,18 @@
"all": "all"}
-CHART_PERIOD_PATTERNS = {
- "day": "%Y%m%d",
- "year": "%Y",
- "month": "%Y%m",
- "hour": "%Y%m%dT%H",
- "minute": "%Y%m%dT%H:%M",
- "all": "all"}
-
-
-
-
STORAGE_SETTINGS_KEY = "STORAGE_SETTINGS"
-
DATE_FILTER_FORMAT = "%Y-%m-%d"
PERIODIC_PROCESSOR_TIME = 1000*60*1
TASKS_CHUNK = 20
+
+
+DEFAULT_EMBEDDED_PARAMS = {
+ "height": 400,
+ "width": 800,
+ "renderer": "line", # area, stack, bar, line, and scatterplot,
+ "interpolation": None, #"linear" # linear, step-after, cardinal, basis
+ }
View
13 gottwall/static/css/embedded.css
@@ -0,0 +1,13 @@
+#chart-container {
+ margin: auto;
+ margin-top: 100px
+ }
+#graph {
+ float: left;
+ }
+#legend {
+ float: right;
+ }
+.clear {
+ clear: both;
+ }
View
4 gottwall/static/css/graph.css
@@ -1,7 +1,3 @@
-.svg-wrapper .y_ticks {
-
-}
-
.rickshaw_graph .y_ticks {
left: 0;
position: absolute;
View
18 gottwall/static/css/main.css
@@ -317,8 +317,7 @@ footer .contribute {
float: right;
}
footer .version, footer .credits {
- float: left;
- margin-right: 20px;
+ float: left;
}
.error-container, .home-container {
@@ -345,4 +344,19 @@ footer .version, footer .credits {
}
.marketing {
margin-bottom: 90px;
+ }
+
+
+#share-modal #share-modal-header {
+ text-align: center;
+ }
+
+#share-modal .modal-body dd{
+ margin-left: 0px;
+ }
+
+#share-modal .modal-body input {
+ margin-bottom: 0px;
+ margin-top: 0px;
+ width: 80%;
}
View
4 gottwall/static/js/app.js
@@ -3,7 +3,7 @@
require.config({
waitSeconds: 15,
- "baseUrl": "/static/",
+ "baseUrl": static_path,
"paths": {
"app": "./app",
"jquery": 'js/jquery',
@@ -93,6 +93,6 @@ require(["jquery", "js/gottwall", "bootstrap", "js/inits/tablesorter", "js/init
console.log();
- self.gottwall = new GottWall(true);
+ self.gottwall = new GottWall(true, prefix);
});
});
View
4 gottwall/static/js/bars/bar.js
@@ -73,11 +73,11 @@ define(["jquery", "underscore", "js/bars/base", "js/metrics/metric","select2"],
if(!this.node){
this.render();
}
- this.node.find('.filters-selector .dropdown-menu').html($(template({
+ this.node.find('.filters-selector .dropdown-menu').html(template({
"filters": _.map(filters, function(value, key){
return [key, value];
})
- })));
+ }));
$(this.node.find('.filters-selector li select')).select2({
placeholder: "Select a State",
View
4 gottwall/static/js/bars/table.js
@@ -56,10 +56,10 @@ define(["jquery", "underscore", "js/bars/base"], function($, _, BaseBar){
}
this.node.find('.filters-selector .dropdown-menu').html(
- $(template({
+ template({
"filters": _.map(filters, function(value, key){
return key;
- })})));
+ })}));
this.node.on('click', '.filters-selector li a', function(){
var button = $(this);
View
42 gottwall/static/js/gottwall.js
@@ -14,12 +14,12 @@ define(["js/class", "js/widgets/chart", "js/widgets/table", "js/bars/bar", "js/b
size_mode_key: "size_mode",
date_formatters: {
- "day": d3.time.format("%Y%m%d"),
+ "day": d3.time.format("%Y-%m-%d"),
"year": d3.time.format("%Y"),
- "month": d3.time.format("%Y%m"),
- "hour": d3.time.format("%Y%m%dT%H"),
- "minute": d3.time.format("%Y%m%dT%H%M"),
- "week": d3.time.format("%Y%W")
+ "month": d3.time.format("%Y-%m"),
+ "hour": d3.time.format("%Y-%m-%dT%H"),
+ "minute": d3.time.format("%Y-%m-%dT%H:%M"),
+ "week": d3.time.format("%Y-%W")
},
date_formats: {
"day": "%Y-%m-%d",
@@ -45,10 +45,11 @@ define(["js/class", "js/widgets/chart", "js/widgets/table", "js/bars/bar", "js/b
"minute": d3.time.format("%Y-%m-%dT%H:%M"),
"week": d3.time.format("%Y-%W")
},
- init: function(debug){
+ init: function(debug, prefix){
this.debug_flag = debug || false;
this.metrics = {};
this.charts = {};
+ this.prefix = prefix;
this.current_project = null;
this.chart_container = $('#chart');
@@ -88,10 +89,31 @@ define(["js/class", "js/widgets/chart", "js/widgets/table", "js/bars/bar", "js/b
}
return null;
},
+ get_embedded_create_url: function(){
+ return "/api/v1/" + this.current_project + "/embedded/";
+ },
+
+ make_embedded: function(data, success){
+ var api_url = this.get_embedded_create_url();
+
+ var self = this;
+ console.debug("Making embedded link");
+
+ $.ajax({
+ type: "POST",
+ url: api_url,
+ data: data,
+ dataType: 'json',
+ success: success,
+ error: function(){
+ $.log(error);
+ }
+ });
+ },
get_metrics_url: function(){
// Metrics structure url
- return this.current_project + "/api/metrics";
+ return "/api/v1/" + this.current_project + "/metrics";
},
metrics_resource_loader: function(){
var self = this;
@@ -378,13 +400,19 @@ define(["js/class", "js/widgets/chart", "js/widgets/table", "js/bars/bar", "js/b
return false;
});
},
+ bind_modal_hide: function(){
+ $('#share-modal').on('hidden', function () {
+ $('#modal-body').html("");
+ });
+ },
add_bindings: function(){
this.bind_period_selectors();
this.bind_project_selector();
this.bind_redraw_button();
this.bind_dates_selectors();
this.bind_resize_button();
this.bind_add();
+ this.bind_modal_hide();
},
save_to_storage: function(){
// Save controls states to localStorage
View
9,411 gottwall/static/js/jquery.js
10 additions, 9,401 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
2 gottwall/static/js/metrics/metric.js
@@ -4,7 +4,7 @@ define(["jquery", "underscore", "js/metrics/base"], function($, _, MetricBase){
load: function(){},
show: function(){},
stats_url: function(){
- var url = this.project + "/api/stats?period="+this.gottwall.current_period+"&name="+this.name;
+ var url = "/api/v1/" + this.project + "/stats?period="+this.gottwall.current_period+"&name="+this.name;
if(this.filter_name && this.filter_value){
url = url + "&filter_name="+this.filter_name+"&filter_value="+this.filter_value;
}
View
2 gottwall/static/js/metrics/set.js
@@ -13,7 +13,7 @@ define(["jquery", "js/metrics/base"], function($, MetricBase){
this.data = data;
},
stats_url: function(){
- var url = this.project + "/api/stats_dataset?period="+this.gottwall.current_period+"&name="+this.name;
+ var url = "/api/v1/" + this.project + "/stats_dataset?period="+this.gottwall.current_period+"&name="+this.name;
if(this.filter_name){
url = url + "&filter_name="+this.filter_name;
}
View
18 gottwall/static/js/widgets/base.js
@@ -55,8 +55,12 @@ define( ["jquery", "js/class", "js/bars/bar", "js/utils/guid", "rickshaw"], func
this.node.on(
'click', '.chart-controls .share-chart', function(){
console.log("Share chart");
- $('#share-modal').modal();
- console.log(self);
+ var params = self.to_dict();
+ params['period'] = self.gottwall.current_period;
+ self.gottwall.make_embedded(JSON.stringify(params),
+ function(data){
+ return self.links_loaded(data);
+ });
});
},
remove: function(){
@@ -65,6 +69,16 @@ define( ["jquery", "js/class", "js/bars/bar", "js/utils/guid", "rickshaw"], func
node_key: function(){
return "chart-"+this.id;
},
+ links_loaded: function(data){
+ console.log(this);
+ console.log(data);
+ this.render_share_modal_body(data);
+ $('#share-modal').modal();
+ },
+ render_share_modal_body: function(data){
+ var template = swig.compile($("#share-modal-body").text());
+ $("#share-modal .modal-body").html(template(data));
+ }
});
return Widget;
});
View
2 gottwall/static/js/widgets/table.js
@@ -48,7 +48,7 @@ var Table = Widget.extend({
return bar_widget;
},
setup_bar: function(bar){
- this.bar = bar
+ this.bar = bar;
},
get_metrics: function(){
var self = this;
View
54 gottwall/static/vendor/rickshaw.js
@@ -1,3 +1,5 @@
+
+
var Rickshaw = {
namespace: function(namespace, obj) {
@@ -150,8 +152,8 @@ function bind(fn, context) {
var emptyFunction = function(){};
var Class = (function() {
-
- // Some versions of JScript fail to enumerate over properties, names of which
+
+ // Some versions of JScript fail to enumerate over properties, names of which
// correspond to non-enumerable properties in the prototype chain
var IS_DONTENUM_BUGGY = (function(){
for (var p in { toString: 1 }) {
@@ -160,7 +162,7 @@ var Class = (function() {
}
return true;
})();
-
+
function subclass() {};
function create() {
var parent = null, properties = [].slice.apply(arguments);
@@ -805,7 +807,7 @@ Rickshaw.Fixtures.RandomData = function(timeInterval) {
data.forEach( function(series) {
var randomVariance = Math.random() * 20;
var v = randomValue / 25 + counter++
- + (Math.cos((index * counter * 11) / 960) + 2) * 15
+ + (Math.cos((index * counter * 11) / 960) + 2) * 15
+ (Math.cos(index / 7) + 2) * 7
+ (Math.cos(index / 17) + 2) * 1;
@@ -927,9 +929,9 @@ Rickshaw.namespace('Rickshaw.Fixtures.Number');
Rickshaw.Fixtures.Number.formatKMBT = function(y) {
abs_y = Math.abs(y);
- if (abs_y >= 1000000000000) { return y / 1000000000000 + "T" }
- else if (abs_y >= 1000000000) { return y / 1000000000 + "B" }
- else if (abs_y >= 1000000) { return y / 1000000 + "M" }
+ if (abs_y >= 1000000000000) { return y / 1000000000000 + "T" }
+ else if (abs_y >= 1000000000) { return y / 1000000000 + "B" }
+ else if (abs_y >= 1000000) { return y / 1000000 + "M" }
else if (abs_y >= 1000) { return y / 1000 + "K" }
else if (abs_y < 1 && y > 0) { return y.toFixed(2) }
else if (abs_y == 0) { return '' }
@@ -1075,7 +1077,7 @@ Rickshaw.Graph.Annotate = function(args) {
var graph = this.graph = args.graph;
this.elements = { timeline: args.element };
-
+
var self = this;
this.data = {};
@@ -1118,7 +1120,7 @@ Rickshaw.Graph.Annotate = function(args) {
if ( box.rangeElement ) box.rangeElement.classList.toggle('active');
});
}, false);
-
+
}
annotation.element.style.left = left + 'px';
@@ -1237,9 +1239,9 @@ Rickshaw.Graph.Axis.Time = function(args) {
var offsets = this.tickOffsets();
offsets.forEach( function(o) {
-
+
if (self.graph.x(o.value) > self.graph.x.range()[1]) return;
-
+
var element = document.createElement('div');
element.style.left = self.graph.x(o.value) + 'px';
element.classList.add('x_tick');
@@ -1552,7 +1554,7 @@ Rickshaw.Graph.Behavior.Series.Order = function(args) {
var self = this;
$(function() {
- $(self.legend.list).sortable( {
+ $(self.legend.list).sortable( {
containment: 'parent',
tolerance: 'pointer',
update: function( event, ui ) {
@@ -1573,7 +1575,7 @@ Rickshaw.Graph.Behavior.Series.Order = function(args) {
});
//hack to make jquery-ui sortable behave
- this.graph.onUpdate( function() {
+ this.graph.onUpdate( function() {
var h = window.getComputedStyle(self.legend.element).height;
self.legend.element.style.height = h;
} );
@@ -1597,12 +1599,12 @@ Rickshaw.Graph.Behavior.Series.Toggle = function(args) {
if (line.series.disabled) {
line.series.enable();
line.element.classList.remove('disabled');
- } else {
+ } else {
line.series.disable();
line.element.classList.add('disabled');
}
}
-
+
var label = line.element.getElementsByTagName('span')[0];
label.onclick = function(e){
@@ -1675,13 +1677,13 @@ Rickshaw.Graph.Behavior.Series.Toggle = function(args) {
this._addBehavior = function() {
this.graph.series.forEach( function(s) {
-
+
s.disable = function() {
if (self.graph.series.length <= 1) {
throw('only one series left');
}
-
+
s.disabled = true;
self.graph.update();
};
@@ -2004,7 +2006,7 @@ Rickshaw.Graph.RangeSlider = function(args) {
range: true,
min: graph.dataDomain()[0],
max: graph.dataDomain()[1],
- values: [
+ values: [
graph.dataDomain()[0],
graph.dataDomain()[1]
],
@@ -2295,7 +2297,7 @@ Rickshaw.Graph.Renderer.Bar = Rickshaw.Class.create( Rickshaw.Graph.Renderer, {
var data = stackedData.slice(-1).shift();
var frequentInterval = this._frequentInterval();
- var barWidth = this.graph.x(data[0].x + frequentInterval.magnitude * (1 - this.gapSize));
+ var barWidth = this.graph.x(data[0].x + frequentInterval.magnitude * (1 - this.gapSize));
return barWidth;
},
@@ -2612,8 +2614,8 @@ Rickshaw.Series = Rickshaw.Class.create( Array, {
this.palette = new Rickshaw.Color.Palette(palette);
- this.timeBase = typeof(options.timeBase) === 'undefined' ?
- Math.floor(new Date().getTime() / 1000) :
+ this.timeBase = typeof(options.timeBase) === 'undefined' ?
+ Math.floor(new Date().getTime() / 1000) :
options.timeBase;
var timeInterval = typeof(options.timeInterval) == 'undefined' ?
@@ -2643,7 +2645,7 @@ Rickshaw.Series = Rickshaw.Class.create( Array, {
} );
} else if (item.data.length == 0) {
item.data.push({ x: this.timeBase - (this.timeInterval || 0), y: 0 });
- }
+ }
this.push(item);
@@ -2663,9 +2665,9 @@ Rickshaw.Series = Rickshaw.Class.create( Array, {
}, this );
this.forEach( function(item) {
- item.data.push({
- x: (index * this.timeInterval || 1) + this.timeBase,
- y: (data[item.name] || 0)
+ item.data.push({
+ x: (index * this.timeInterval || 1) + this.timeBase,
+ y: (data[item.name] || 0)
});
}, this );
},
@@ -2751,7 +2753,7 @@ Rickshaw.Series.fill = function(series, fill) {
while ( i < Math.max.apply(null, data.map( function(d) { return d.length } )) ) {
- x = Math.min.apply( null,
+ x = Math.min.apply( null,
data
.filter(function(d) { return d[i] })
.map(function(d) { return d[i].x })
View
4 gottwall/storages/base.py
@@ -35,6 +35,10 @@ def setup(cls, application):
application.storage = storage
return storage
+ def make_embedded(self, project, metrics):
+ """Make embedded hash for metrics
+ """
+ raise NotImplementedError
def incr(self, project, name, timestamp, value=1, filters=None, **kwargs):
"""Add count for metric `name` and `filters`
View
22 gottwall/storages/memory.py
@@ -6,11 +6,12 @@
Memory storage for collect statistics
-:copyright: (c) 2012 by GottWall team, see AUTHORS for more details.
+:copyright: (c) 2012 - 2013 by GottWall team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
-:github: http://github.com/Lispython/GottWall
+:github: http://github.com/GottWall/GottWall
"""
+import uuid
from itertools import ifilter
from base import BaseStorage
from gottwall.utils import get_by_period, MagicDict, get_datetime
@@ -25,6 +26,7 @@ def __init__(self, application):
super(MemoryStorage, self).__init__(application)
self._store = MagicDict()
self._metrics = {}
+ self._embedded = {}
def load_db(self, f):
"""Load data from f
@@ -38,6 +40,22 @@ def save_db(self, f):
"""
raise RuntimeError
+ def make_embedded(self, project, metrics=[]):
+ """Save metrics for share key
+ """
+ uid = uuid.uuid4()
+ self._embedded[uid] = {
+ "project": project,
+ "metrics": metrics}
+ return uid
+
+ def get_embedded(self, h):
+ """Get metrics from share key
+ """
+ if h not in self._embedded:
+ return None
+ return self._embedded[h]
+
def save_value(self, project, name, period, timestamp, fname=None, fvalue=None, value=1):
"""Save value to store dict
View
59 gottwall/storages/redis.py
@@ -10,9 +10,11 @@
:license: BSD, see LICENSE for more details.
:github: http://github.com/gottwall/gottwall
"""
+import uuid
from logging import getLogger
import tornado.gen
+from tornado.escape import json_decode, json_encode
import tornadoredis
from tornado import gen
from tornado.gen import Task
@@ -54,18 +56,71 @@ class RedisStorage(BaseStorage):
def __init__(self, application):
super(RedisStorage, self).__init__(application)
config = self._application.config
+ self.selected_db = int(config[STORAGE_SETTINGS_KEY].get('DB', 0))
self.client = Client(
host=config[STORAGE_SETTINGS_KEY].get('HOST', 'localhost'),
port=int(config[STORAGE_SETTINGS_KEY].get('PORT', 6379)),
password=config[STORAGE_SETTINGS_KEY].get('PASSWORD', None),
- selected_db=int(config[STORAGE_SETTINGS_KEY].get('DB', 0)))
+ selected_db=self.selected_db)
logger.info("Redis storage client {0}".format(repr(self.client)))
self.client._reconnect_callback = self.client.connect
self.client.connect()
- self.client.select(self.client.selected_db)
+ self.client.select(self.selected_db)
+
+ @gen.engine
+ def make_embedded(self, project, period, metrics=[], callback=None):
+ """Save chart data for sharings
+
+ :param project: project name
+ :param metrics: list of metrics
+ :param callback: callback function for async call
+ """
+ uid = uuid.uuid4()
+
+ for i, metric in enumerate(metrics):
+
+ metric['fn'] = metric.pop('filter_name', None)
+ metric['m'] = metric.pop('metric_name', None)
+ metric['fv'] = metric.pop('filter_value', None)
+
+ for k, v in metric.items():
+ if (k not in ['m', 'fn', 'fv']) or not v:
+ del metric[k]
+
+ data = {"project": project,
+ "metrics": metrics,
+ "period": period}
+
+ json_data = json_encode(data)
+ res = (yield gen.Task(self.client.set, self.make_embedded_key(uid), json_data))
+
+ if callback:
+ callback(uid if res else None)
+
+ @gen.engine
+ def get_embedded(self, uid, callback=None):
+ """Get share data from storage by hash
+ """
+ try:
+ json_data = (yield gen.Task(self.client.get, self.make_embedded_key(uid)))
+ data = json_decode(json_data)
+ except Exception:
+ data = {}
+
+ if callback:
+ callback(data)
+
+ @staticmethod
+ def make_embedded_key(uid):
+ """Make key for embedded chart data
+
+ :param uid: unique embedded chart identificator string
+ :return: key string
+ """
+ return "embedded-{0}".format(uid)
@gen.engine
def save_metric_meta(self, pipe, project, name,
View
9 gottwall/templates/base.html
@@ -3,7 +3,9 @@
<head>
<title>{% block title %}{{ handler.settings['site_title'] }}{% endblock %}</title>
<meta name="description" content="{{ handler.settings['site_title'] }}"/>
- <meta name="version" content="{{ version }}"/>
+ <meta name="title" content="{{ handler.settings['site_title'] }}" />
+ <meta name="generator" content="{{ generator }}"/>
+
<link rel="stylesheet" type="text/css" media="all" href="{{ static }}vendor/bootstrap/css/bootstrap.css"/>
<link rel="stylesheet" type="text/css" media="all" href="{{ static }}css/tablesorter.bootstrap.css"/>
<link rel="stylesheet" type="text/css" media="all" href="{{ static }}css/main.css"/>
@@ -76,9 +78,8 @@
<footer>
<div class="container">
- <div class="contribute">GottWall is open source software <a href="https://github.com/GottWall/GottWall" class="btn btn-small">Contribute</a></div>
- <div class="version">GottWall {{ version }}</div>
- <div class="credits">Created by the <a href="https://github.com/GottWall?tab=members">GottWall</a> team with <a href="https://github.com/GottWall/GottWall/contributors">lots of help</a></div>
+ <div class="contribute">{% trans %}GottWall is open source software <a href="https://github.com/GottWall/GottWall" class="btn btn-small">Contribute</a>{% endtrans %}</div>
+ <div class="credits">{% trans %}<span class="version">{{ generator }} </span> &mdash; created by the <a href="https://github.com/GottWall?tab=members">GottWall</a> team with <a href="https://github.com/GottWall/GottWall/contributors">lots of help</a>{% endtrans %}</div>
</div>
</footer>
View
31 gottwall/templates/dashboard.html
@@ -4,7 +4,10 @@
{% block head %}
-
+<script>
+var static_path = "{{ static }}";
+var prefix = "{{ handler.settings['PREFIX'] }}";
+</script>
<script data-main="{{ static }}js/app" src="{{ static }}vendor/require.js"></script>
{#<script type="text/javascript" src="{{ static }}vendor/moment.min.js"></script>#}
@@ -150,8 +153,7 @@
<script type="text/html" id="filters-selector-template">
{% raw %}
{% for filter in filters %}
- <li>
- {% set values = filter[1] %}
+ <li>{% set values = filter[1] %}
<select data-placeholder="{{ filter[0] }}" placeholder="{{ filter[0] }}" class="placeholder populate"><option></option>{% for value in values %}<option value="{{ value }}" data-filter-name="{{ filter[0] }}">{{ value }}</option>{% endfor %}</select>
</li>{% endfor %}
{% endraw %}
@@ -206,17 +208,30 @@
{% endraw %}
</script>
+<script type="text/html" id="share-modal-body">
+ {% raw %}
+ <dl>
+ <dt title="{% endraw %}Single page html version{% raw %}">{% endraw %}{{ _("Html:") }}{% raw %}</dt>
+ <dd><input type="text" value="{{ html_link }}" data-type="html"></dd>
+ <dt title="{% endraw %}Iframe tag{% raw %}">{% endraw %}{{ _("Iframe:") }}{% raw %}</dt>
+ <dd><input type="text" value="{{ iframe }}" data-type="iframe"></dd>
+ <dt title="{% endraw %}Link to javascript version for javascript tag{% raw %}">{% endraw %}{{ _("JavaScript:") }}{% raw %}</dt>
+ <dd><input type="text" value="{{ js_link }}" data-type="javascript"></dd>
+ <dt title="{% endraw %}JSON data response{% raw %}">{% endraw %}{{ _("JSON:") }}{% raw %}</dt>
+ <dd><input type="text" value="{{ json_link }}" data-type="json"></dd>
+ </dl>
+ {% endraw %}
+</script>
+
+
<div id="share-modal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
- <h3 id="share-modal-header">Modal header</h3>
- </div>
- <div class="modal-body">
- <p>One fine body…</p>
+ <h3 id="share-modal-header">{{ _("Share chart") }}</h3>
</div>
+ <div class="modal-body"></div>
<div class="modal-footer">
<button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
- <button class="btn btn-primary">Save changes</button>
</div>
</div>
{% endblock %}
View
150 gottwall/templates/embedded.css
@@ -0,0 +1,150 @@
+#chart-container {
+ margin: auto;
+ }
+#graph {
+ float: left;
+ }
+#legend {
+ float: lfet;
+ }
+.clear {
+ clear: both;
+ }
+
+#y_axis {
+ width: 40px;
+ float: left;
+ }
+
+#x_axis {
+ float: left;
+ margin-left: 40px;
+ }
+
+footer {
+margin-top: 15px;
+color: #aaa;
+ float: left;
+ clear: both;
+ }
+
+content h2, content h3 {
+ width: 100%;
+ text-align: center;
+ color: #333;
+ font-family: Georgia, serif, "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif;
+ text-align: center;
+ font-style: italic;
+ font-weight: normal;
+ margin-bottom: 10px;
+ }
+content {
+float: left;
+ }
+
+content h3 {
+ font-style: normal;
+ margin-top: 0px;
+ }
+
+
+.rickshaw_graph .detail .x_label {
+ display: none }
+
+
+#sidebar {
+ float: left;
+ margin-left: 15px;
+ }
+#renderers {
+ margin-top: 20px;
+ float: left;
+ }
+
+#renderers_form ul {
+ margin-left: 0px;
+ padding-left: 0px;
+ }
+#renderers_form ul li {
+
+ list-style: none;
+ }
+
+.rickshaw_graph .detail {
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ z-index: 2;
+ background: rgba(0, 0, 0, 0.1);
+ bottom: 0;
+ width: 1px;
+ transition: opacity 0.25s linear;
+ -moz-transition: opacity 0.25s linear;
+ -o-transition: opacity 0.25s linear;
+ -webkit-transition: opacity 0.25s linear;
+}
+.rickshaw_graph .detail.inactive {
+ opacity: 0;
+}
+.rickshaw_graph .detail .item.active {
+ opacity: 1;
+}
+.rickshaw_graph .detail .x_label {
+ font-family: Arial, sans-serif;
+ border-radius: 3px;
+ padding: 6px;
+ opacity: 0.5;
+ border: 1px solid #e0e0e0;
+ font-size: 12px;
+ position: absolute;
+ background: white;
+ white-space: nowrap;
+}
+.rickshaw_graph .detail .item {
+ position: absolute;
+ z-index: 2;
+ border-radius: 3px;
+ padding: 0.25em;
+ font-size: 12px;
+ font-family: Arial, sans-serif;
+ opacity: 0;
+ background: rgba(0, 0, 0, 0.4);
+ color: white;
+ border: 1px solid rgba(0, 0, 0, 0.4);
+ margin-left: 1em;
+ margin-top: -1em;
+ white-space: nowrap;
+}
+.rickshaw_graph .detail .item.active {
+ opacity: 1;
+ background: rgba(0, 0, 0, 0.8);
+}
+.rickshaw_graph .detail .item:before {
+ content: "\25c2";
+ position: absolute;
+ left: -0.5em;
+ color: rgba(0, 0, 0, 0.7);
+ width: 0;
+}
+.rickshaw_graph .detail .dot {
+ width: 4px;
+ height: 4px;
+ margin-left: -4px;
+ margin-top: -3px;
+ border-radius: 5px;
+ position: absolute;
+ box-shadow: 0 0 2px rgba(0, 0, 0, 0.6);
+ background: white;
+ border-width: 2px;
+ border-style: solid;
+ display: none;
+ background-clip: padding-box;
+}
+.rickshaw_graph .detail .dot.active {
+ display: block;
+}
+
+.rickshaw_graph .detail .x_label { display: none }
+.rickshaw_graph .detail .item { line-height: 1.4; padding: 0.5em }
+.detail_swatch { float: right; display: inline-block; width: 10px; height: 10px; margin: 0 4px 0 0 }
+.rickshaw_graph .detail .date { color: #a0a0a0 }
View
159 gottwall/templates/embedded.html
@@ -0,0 +1,159 @@
+{% macro render_metric(metric) -%}
+{%- set filter_name = metric["filter_name"] -%}
+{%- set filter_value = metric["filter_value"] -%}
+{"name": "{{ metric["name"] }}{% if filter_name and filter_value %} | {{ filter_name }}:{{ filter_value }}{% endif %}",
+ "color": palette.color(),
+ "data": [{% for item in metric["range"] -%}{"x": {{ x_converter(item[0]) }}, "y": {{ item[1] }}}{% if not loop.last %},{% endif %}{%- endfor %}]
+}
+{% endmacro %}
+
+{% macro renderer_item(r) %}
+<input type="radio" value="{{ r }}" id="{{ r }}" {% if renderer == r %}checked{% endif %}> &mdash;
+<label class="lines" for="lines">{{ r }}</label><br>
+{% endmacro %}
+
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>{{ handler.settings["site_title"] }} → {{ data["name"] }}</title>
+ <meta name="description" content="{{ handler.settings["site_title"] }} → {{ data["name"] }}"/>
+ <meta name="title" content="{{ handler.settings["site_title"] }}" />
+ <meta name="generator" content="{{ generator }}" />
+
+ <link rel="stylesheet" type="text/css" media="all" href="{{ static }}css/rickshaw.min.css"/>
+ <link rel="stylesheet" type="text/css" media="all" href="{{ static }}css/graph.css"/>
+ <script type="text/javascript" src="{{ static }}js/jquery.js"></script>
+ <script type="text/javascript" src="{{ static }}js/d3.v2.js"></script>
+ <script type="text/javascript" src="{{ static }}vendor/rickshaw.js"></script>
+ <style type="text/css" media="screen">
+ {% include "embedded.css" %}
+ </style>
+ </head>
+ <body>
+ <content>
+ <div id="chart-container">
+ <h2 style="width: {{ width+40 }}px;">{{ data["name"] }}</h2>
+ <h3 style="width: {{ width+40 }}px;">{{ data["from_date"] }} &mdash; {{ data["to_date"] }}</h3>
+ <div id="y_axis" style="height: {{height}}px"></div>
+ <div id="graph" style="height:{{ height }}px; width: {{ width }}px;"></div>
+ <div id="sidebar">
+ <div id="legend"></div>
+ <div id="renderers">
+ <form id="renderers_form" class="toggler">
+ <ul>
+ <li>{{ renderer_item("line") }}</li>
+ <li>{{ renderer_item("stack") }}</li>
+ <li>{{ renderer_item("area") }}</li>
+ <li>{{ renderer_item("bar") }}</li>
+ <li>{{ renderer_item("scatterplot") }}</li>
+ </form>
+ </div>
+ </div>
+ <div id="x_axis" style="width: {{ width }}px;"></div>
+ </div>
+ </content>
+ {% block footer %}
+ <footer>
+ <div class="generator">{% trans generator=data["generator"], site_name=handler.settings["site_title"] -%}
+ Generated by <a href="{{ gottwall_home }}" title="{{ gottwall_description }}">{{ generator }}</a> for {{ site_name }}{%- endtrans -%}
+ </div>
+ </footer>
+ {% endblock %}
+
+ <script type="text/javascript">
+ var date_display_formatters = {
+ "day": d3.time.format("%Y-%m-%d, %A"),
+ "year": d3.time.format("%Y"),
+ "month": d3.time.format("%Y-%m, %B"),
+ "hour": d3.time.format("%Y-%m-%dT%H"),
+ "minute": d3.time.format("%Y-%m-%dT%H:%M"),
+ "week": d3.time.format("%Y-%W")
+ }
+
+ var date_formatter = date_display_formatters["{{ data["period"] }}"];
+ console.log(date_formatter);
+
+ var timestamp_to_date = function(timestamp){
+ return new Date(timestamp * 1000);
+ }
+
+ var pretty_date_format = function(d){
+ try {
+ return date_formatter(timestamp_to_date(d));
+ }
+ catch (e){
+ return "";
+ }
+
+ }
+ var palette = new Rickshaw.Color.Palette();
+
+ var graph = new Rickshaw.Graph( {
+ padding: {
+ top: 0.25,
+ bottom: 0.5
+ },
+ height: {{ height }},
+ width: {{ width }},
+ renderer: "{{ renderer }}",
+ {% if interpolation %}interpolation: "{{ interpolation }}",{% endif %}
+ element: document.querySelector("#graph"),
+ series: [{% for metric in data["metrics"] -%}{{ render_metric(metric) -}}{%- if not loop.last -%},{%- endif -%}{%- endfor %}]
+ });
+
+ var legend = new Rickshaw.Graph.Legend({
+ graph: graph,
+ element: document.querySelector("#legend")
+ });
+
+
+
+ var x_axis = new Rickshaw.Graph.Axis.X({
+ height: 20,
+ graph: graph,
+ orientation: "top",
+ //pixelsPerTick: 100,
+ //element: document.getElementById("x_axis"),
+ tickFormat: pretty_date_format,
+ });
+
+ var y_axis = new Rickshaw.Graph.Axis.Y({
+ graph: graph,
+ orientation: "left",
+ element: document.getElementById("y_axis"),
+ tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
+ });
+
+ var shelving = new Rickshaw.Graph.Behavior.Series.Toggle( {
+ graph: graph,
+ legend: legend
+} );
+
+ var highlighter = new Rickshaw.Graph.Behavior.Series.Highlight({
+ graph: graph,
+ legend: legend
+ });
+
+
+ var hoverDetail = new Rickshaw.Graph.HoverDetail({
+ graph: graph,
+ formatter: function(series, x, y) {
+ var date = "<span class=\"date\">" + pretty_date_format(x) + "</span>";
+ var swatch = "<span class=\"detail_swatch\" style=\"background-color: " + series.color + "\"></span>";
+ var content = swatch + series.name + "" + parseInt(y) + "<br>" + date;
+ return content;
+ }
+ });
+
+ var renderer_form = document.getElementById("renderers_form");
+
+ renderer_form.addEventListener("change", function(e) {
+ graph.setRenderer(e.target.value);
+
+ graph.render();
+
+}, false);
+ graph.render();
+ </script>
+</body>
+</html>
View
1 gottwall/templates/js_embedded.html
@@ -0,0 +1 @@
+document.write('<iframe src="//{{ request.host }}{{ reverse('api-v1-html-embedded', uid)}}?width={{ width }}&height={{ height }}&from_date={{ from_date }}&to_date={{ to_date }}&renderer={{ renderer }}&interpolation={{ interpolation }}&period={{ period }}" width="{{ width }}" height="{{ height }}"></iframe>');
View
12 gottwall/utils.py
@@ -11,6 +11,7 @@
:github: http://github.com/gottwall/gottwall
"""
import os.path
+from time import mktime
from datetime import datetime, timedelta, date
from urllib2 import parse_http_list
from dateutil.relativedelta import relativedelta
@@ -31,7 +32,7 @@
pass
-from settings import PROJECT_ROOT, TIMESTAMP_FORMAT, PERIOD_PATTERNS, CHART_PERIOD_PATTERNS
+from settings import PROJECT_ROOT, TIMESTAMP_FORMAT, PERIOD_PATTERNS
__all__ = 'rel',
@@ -63,7 +64,7 @@ def format_date_by_period(dt, period):
if period == "week":
return "{0}-{1}".format(ts.year, ts.isocalendar()[1])
elif period in PERIOD_PATTERNS:
- return ts.strftime(CHART_PERIOD_PATTERNS[period])
+ return ts.strftime(PERIOD_PATTERNS[period])
return None
def get_by_period(dt, period):
@@ -78,7 +79,7 @@ def get_by_period(dt, period):
if period == "week":
return "{0}-{1}".format(ts.year, ts.isocalendar()[1])
elif period in PERIOD_PATTERNS:
- return ts.strftime(CHART_PERIOD_PATTERNS[period])
+ return ts.strftime(PERIOD_PATTERNS[period])
return None
@@ -102,6 +103,11 @@ def get_datetime(timestamp, period):
return datetime.strptime(timestamp, PERIOD_PATTERNS[period])
return None
+def datetime_to_utimestamp(dt):
+ """Convert datetime object to unix timestamp
+ """
+ return int(mktime(dt.timetuple()))
+
def parse_dict_header(value):
"""Parse key=value pairs from value list

0 comments on commit bab21b8

Please sign in to comment.