├── .gitignore
├── README.md
├── assets
├── doc_amount_line_chart.png
├── doc_amount_pie_chart.png
├── doc_amount_today_count.png
├── doc_amount_total_count.png
├── doc_datalist_list.png
├── doc_realtime_count.png
├── doc_realtime_line_chart.png
├── preview.png
└── preview_thumbnail.png
├── config_default.yaml
├── load_config.js
├── middleware.js
├── package.json
├── public
├── favicon.ico
├── images
│ ├── bg.jpg
│ ├── col.gif
│ ├── trend-down.gif
│ └── trend-up.gif
├── img
│ ├── glyphicons-halflings-white.png
│ └── glyphicons-halflings.png
├── javascripts
│ ├── lib
│ │ ├── async.js
│ │ ├── bootstrap.js
│ │ ├── highcharts.js
│ │ ├── highcharts_exporting.js
│ │ ├── jquery.js
│ │ ├── mustache.js
│ │ └── underscore.js
│ └── ranaly.js
├── kalendae
│ ├── arrows.png
│ ├── close.png
│ ├── kalendae.css
│ ├── kalendae.js
│ └── kalendae.min.js
└── stylesheets
│ ├── bootstrap.min.css
│ └── style.css
├── ranaly.js
├── template
├── default.mustache
└── images.mustache
└── views
├── error.jade
├── index.jade
├── layout.jade
├── login.jade
├── page.jade
├── page_layout.jade
└── widgets
├── amount_line_chart.jade
├── amount_pie_chart.jade
├── amount_today_count.jade
├── amount_total_count.jade
├── custom_code.jade
├── datalist_list.jade
├── realtime_count.jade
└── realtime_line_chart.jade
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | _*
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ranaly
2 | Ranaly是一个基于Redis的数据统计可视化工具。
3 |
4 | Ranaly的Node.js客户端库[node_ranaly](https://github.com/luin/node_ranaly)已经完成。
5 |
6 | 
7 |
8 | 特点如下:
9 |
10 | 1. 使用简单,容易维护
11 | 2. 基于Redis,性能优异
12 | 3. 支持模块级别的权限控制
13 | 4. 长连接实时数据统计
14 | 5. 多种图表可以选择,可以自定义模板、模块
15 |
16 | ## 快速入门
17 | 在项目中使用Ranaly统计数据十分简单,步骤如下。
18 | ### 1. 安装Node.js和Redis
19 | Ranaly使用Node.js开发,所以需要先安装[Node.js](http://nodejs.org/)。同样因为Ranaly的统计数据存储于Redis中,所以需要安装[Redis](http://redis.io/)。
20 | ### 2. 安装Ranaly
21 |
22 | git clone git://github.com/luin/ranaly.git
23 | cd ranaly
24 | npm install
25 |
26 | ### 3. 在已有项目中加入统计代码
27 | 如果你的项目使用Node.js开发,可以使用Ranaly的node客户端库,安装方法如下:
28 |
29 | npm install node_ranaly
30 |
31 | 如果希望统计项目注册用户的变化趋势,可以在用户注册成功后加上如下代码:
32 |
33 | var ranaly = require('node_ranaly').createClient();
34 | var rUsers = new ranaly.Amount('Users');
35 | rUsers.incr();
36 |
37 | node_ranaly库会将名为“Users”的Amount类型的桶的值增1并和当前时间一起写入到Redis中。
38 |
39 | ### 4. 查看统计结果
40 | 建立配置文件,内容如下:
41 |
42 | app_name: Demo
43 | users:
44 | - username: admin
45 | password: admin123
46 | pages:
47 | - title: Overview
48 | widgets:
49 | - type: amount_line_chart
50 | bucket: Users
51 |
52 | 将文件保存,并进入Ranaly的目录执行:
53 |
54 | node ranaly /path/to/config_file
55 |
56 | 其中`/path/to/config_file`表示配置文件路径。此时就可以访问 http://127.0.0.1:3000 进入数据查看页面了,使用admin和admin123登录系统,能看到用户数量的折线图。
57 |
58 | ## 文档
59 | Ranaly由两个部分组成,分别是客户端库和数据展示部分,本页面项目是数据展示部分。在程序中通过客户端库在Redis中记录统计数据,而本页面项目的作用是将这些数据以图表的形式显示出来。
60 |
61 | ### 1. 客户端库
62 | Node.js:[node_ranaly](https://github.com/luin/node_ranaly)
63 |
64 | ### 2. 数据类型
65 | 为了适应不同场合的统计需要,Ranaly支持3种类型的数据统计:
66 |
67 | #### (1) Amount
68 | 当要记录某一个数据的在不用时间的数量变化时就需要使用Amount类型,如想记录用户数量的变化趋势等。
69 |
70 | #### (2) Realtime
71 | Realtime用来记录实时数据,如当前系统内存使用情况等。
72 |
73 | #### (3) DataList
74 | DataList用来记录数据列表,数据可以是数字、字符串甚至对象。如新注册的用户头像地址列表、新注册的用户资料列表等。DataList只保留数据列表中的前N条数据(N的数值可以指定)。
75 |
76 | ### 3. Ranaly配置文件
77 | 为了能够通过Ranaly来将Redis中记录的统计数据可视化,需要提供一份配置文件。配置文件的格式为YAML,样例如下:
78 |
79 | app_name: 应用名称
80 | port: Ranaly使用的端口号,默认是3000
81 | redis:
82 | host: Redis的主机地址,默认是127.0.0.1
83 | port: Redis的端口号,默认是6379
84 | key_prefix: 存储统计数据的键名前缀,需要和客户端库的配置一样
85 | users:
86 | - username: 用户名
87 | password: 密码
88 | role: 数字,表示用户的权限,默认是0
89 | pages:
90 | - title: 页面的标题
91 | widgets:
92 | - type: widget的类型
93 | bucket: widget对应的bucket
94 | title: widget的标题
95 | role: 数字,只对拥有大于或等于该role的用户可见
96 |
97 | ### 4. 桶(bucket)
98 | 为了区分不同的统计数据,需要为每类数据起个名字(桶),如统计用户名称的桶可以命名为“Users”,统计访问量的桶可以命名为“Page views”。不同数据类型的桶可以重名,桶的名称可以包含空格。
99 |
100 | ### 5. Widget类型
101 | 在配置文件中可以看到每一个项目是由若干个page组成的,每个page由若干个widget组成。widget分为不同种类,每一种widget只适用于一种数据类型,可以从其命名看出来。
102 |
103 | #### (1) amount_line_chart
104 | 该类型的widget用来显示折线图,只支持Amount。每个widget除了type、bucket、title和role四个参数外还支持其它不同的参数。amount_line_chart类型支持的参数如下:
105 |
106 | | 参数名 | 意义 | 取值 |
107 | |---------------|-----------|-----------------------------------------------------------------|
108 | | default_range | 默认显示的时间范围 | today(显示当天的数据),yesterday(昨天的数据),7days(最近7天的数据),30days(最近30天的数据) |
109 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 |
110 |
111 |
112 | 
113 |
114 | amount_line_chart支持同时显示多个bucket的数据,在配置文件中以数组形式设置,如:
115 |
116 | type: amount_line_chart
117 | bucket: [Users, Page views, Groups]
118 |
119 | #### (2) amount_pie_chart
120 | 用来显示饼图。amount_pie_chart类型支持的其它参数如下:
121 |
122 | | 参数名 | 意义 | 取值 |
123 | |---------------|-----------|---------------------------------------------------------------------------|
124 | | default_range | 默认显示的时间范围 | today(显示当天的数据),yesterday(昨天的数据),7days(最近7天的数据),30days(最近30天的数据),all(所有数据) |
125 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 |
126 |
127 |
128 | amount_pie_chart支持同时比对多个bucket的数据。
129 |
130 | 
131 |
132 | #### (3) amount_today_count
133 | 用来显示当天的数据,并根据昨天同时间的数据预测今天的全天的数据。支持的其它参数如下:
134 |
135 | | 参数名 | 意义 | 取值 |
136 | |---------------|-----------|---------------------------------------------------------------------------|
137 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 |
138 |
139 |
140 | 
141 |
142 | amount_today_count支持同时显示多个bucket的数据。
143 |
144 | #### (4) amount_total_count
145 | 显示某一时间范围的数据总和,如最近30天注册的用户总数。支持的其它参数如下:
146 |
147 | | 参数名 | 意义 | 取值 |
148 | |---------------|-----------|---------------------------------------------------------------------------|
149 | | update_interval | 数据更新间隔,默认为20-40秒 | 数字,单位为秒 |
150 |
151 |
152 | 
153 |
154 | amount_total_count支持同时显示多个bucket的数据。
155 |
156 | #### (5) realtime_line_chart
157 | 显示实时数据的折线图,更新频率是1秒。
158 |
159 | 
160 |
161 | realtime_line_chart支持同时显示多个bucket的数据。
162 |
163 | #### (6) realtime_count
164 | 显示实时数据的数值,更新频率是实时。
165 |
166 | 
167 |
168 | realtime_count支持同时显示多个bucket的数据。
169 |
170 | #### (7) datalist_list
171 | 显示DataList数据列表。由于DataList类型可以存储任何数据的列表,所以该类型的widget支持高度自定义。可以通过template参数指定显示DataList的模板,模板采用[Mustache](http://mustache.github.com/),渲染的数据格式是{"data": [*数据列表*]}。下面的实例中还会对此进行介绍。除此之外还支持count_per_page参数用来指定每页要显示的数据条数。
172 |
173 | 
174 |
175 | #### (8) custom_code
176 | 该widget类型比较特殊,无需bucket参数,只需要content参数。作用是执行自定义的代码,包括HTML/CSS/JavaScript。如:
177 |
178 | - type: custom_code
179 | content: >
180 |
hi
181 |
182 | ## 综合实例
183 | 现在假设要统计Facebook网站的数据,以使用Ranaly的Node.js客户端为例。首先我们通过node_ranaly建立到Redis的连接(假设Redis运行在本机6380端口上):
184 |
185 | var ranaly = require('node_ranaly');
186 | var ranalyClient = ranaly.createClient(6380, '127.0.0.1');
187 |
188 | 首先要统计的是用户的数量,每当用户注册成功都调用如下代码令Users桶的数值加1:
189 |
190 | var rUsers = new ranalyClient.Amount('Users');
191 | rUsers.incr();
192 |
193 | 然后我们要统计网站的访问量,每当访问一个页面时,都执行:
194 |
195 | var rPageViews = new ranalyClient.Amount('Page views');
196 | rPageViews.incr();
197 |
198 | 现在来配置Ranaly可视化部分来显示我们的统计数据:
199 |
200 | app_name: Facebook
201 | redis:
202 | host: 127.0.0.1
203 | port: 6380
204 | users:
205 | - username: admin
206 | password: admin123
207 | pages:
208 | - title: Overview
209 | widgets:
210 | - type: amount_line_chart
211 | bucket: [Users, Page views]
212 |
213 | 在这个配置中,我们使用折线图来比对用户数量和访问量的关系。将该内容存为config.yaml,然后执行:
214 |
215 | $ node ./ranaly.js /path/to/config.yaml
216 | Ranaly server listening on port 3000
217 |
218 | 此时就可以通过http://127.0.0.1:3000来访问了。
219 |
220 | 在Facebook中,用户可以发布文字状态、照片和视频,若想统计这3种类型的状态的比例,可以在发布时执行:
221 |
222 | var bucket = '';
223 | switch (status.type) {
224 | case 'text':
225 | bucket = 'Text';
226 | break;
227 | case 'photo':
228 | bucket = 'Photo';
229 | break;
230 | case 'video':
231 | bucket = 'Video';
232 | break;
233 | }
234 | var rStatus = new ranalyClient.Amount(bucket);
235 | rStatus.incr();
236 |
237 | 然后我们接着编辑config.yaml,在widgets中加入:
238 |
239 | - type: amount_pie_chart
240 | bucket: [Text, Photo, Video]
241 |
242 | 这时重新启动Ranaly服务器(每次修改config.yaml都得重启),可以看到显示3者比例的饼图,并且可以随意调整时间范围。
243 |
244 | 接下来显示服务器的资源,包括内存和CPU:
245 |
246 | var rMemory = new ranalyClient.Realtime('Memory');
247 | var rCPU = new ranalyClient.Realtime('CPU');
248 | setInterval(function () {
249 | rMemory.set(System.getUsedMemory());
250 | rCPU.set(System.getUsedCPU());
251 | }, 100);
252 |
253 | 嗯...好像是没有System.getUsedMemory()和System.getUsedCPU()这两个东西,不过只要知道他们都会返回个数字就好了。
254 |
255 | 同样我们接着编辑config.yaml,在widgets中加入:
256 |
257 | - type: realtime_count
258 | bucket: [Memory, CPU]
259 |
260 | 重启服务器,就可以看到空闲的内存和CPU资源了。每次Realtime类型的数据更新都会推送给Ranaly服务器,所以页面上显示的数值每100毫秒变一次。
261 |
262 | 现在我们还希望能在Ranaly页面中看到用户最新上传的头像,所以我们在头像上传成功后执行:
263 |
264 | var rAvatar = new ranalyClient.DataList('Avatar');
265 | rAvatar.push(avatarURL, 100);
266 |
267 | 其中`avatarURL`是一个字符串,存储用户头像的URL地址。100表示只存储最新的100条记录,防止数据占用过多的内存(因为数据是存储在Redis中的)。
268 |
269 | 接着我们来配置config.yaml:
270 |
271 | - type: datalist_list
272 | bucket: [Avatar]
273 |
274 | 默认会以列表的形式展示数据,不是很好看,所以我们来自定义模板。Ranaly使用了BootStrap框架,所以可以在模板中自由使用BootStrap的风格。我们将数据修改成:
275 |
276 | - type: datalist_list
277 | bucket: [Avatar]
278 | template: >
279 |
280 | {{#data}}
281 | -
282 |
283 |
284 |
285 |
286 | {{/data}}
287 |
288 |
289 | 对于展示图片,Ranaly提供了一个预置的模板,可以这样写:
290 |
291 | - type: datalist_list
292 | bucket: [Avatar]
293 | preset_template: images
294 |
295 | 接着我们想记录最近注册成功的用户的资料,则在用户注册成功后执行:
296 |
297 | var rUsers = new ranalyClient.DataList('Users');
298 | rUsers.push({
299 | id: user.id,
300 | name: user.name,
301 | age: user.age,
302 | description: user.description
303 | }, 100);
304 |
305 | 然后修改配置文件:
306 |
307 | - type: datalist_list
308 | bucket: [Users]
309 | template:
310 |
311 |
312 |
313 | ID |
314 | name |
315 | age |
316 | description |
317 |
318 |
319 |
320 | {{#data}}
321 |
322 | {{id}} |
323 | {{name}} |
324 | {{age}} |
325 | {{description}} |
326 |
327 | {{/data}}
328 |
329 |
330 |
331 | 就可以看到记录最新注册用户资料的表格了。
332 |
333 |
334 | [](https://bitdeli.com/free "Bitdeli Badge")
335 |
336 |
--------------------------------------------------------------------------------
/assets/doc_amount_line_chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_line_chart.png
--------------------------------------------------------------------------------
/assets/doc_amount_pie_chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_pie_chart.png
--------------------------------------------------------------------------------
/assets/doc_amount_today_count.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_today_count.png
--------------------------------------------------------------------------------
/assets/doc_amount_total_count.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_amount_total_count.png
--------------------------------------------------------------------------------
/assets/doc_datalist_list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_datalist_list.png
--------------------------------------------------------------------------------
/assets/doc_realtime_count.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_realtime_count.png
--------------------------------------------------------------------------------
/assets/doc_realtime_line_chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/doc_realtime_line_chart.png
--------------------------------------------------------------------------------
/assets/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/preview.png
--------------------------------------------------------------------------------
/assets/preview_thumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/assets/preview_thumbnail.png
--------------------------------------------------------------------------------
/config_default.yaml:
--------------------------------------------------------------------------------
1 | app_name: My App
2 | port: 3000
3 | redis:
4 | host: 127.0.0.1
5 | port: 6379
6 | key_prefix: 'Ranaly:'
7 | users:
8 | - username: admin
9 | password: admin123
10 | role: 0
11 | pages:
12 | # Add your pages here.
13 |
--------------------------------------------------------------------------------
/load_config.js:
--------------------------------------------------------------------------------
1 | var yaml = require('js-yaml');
2 | var fs = require('fs');
3 |
4 | if (process.argv[2] && fs.existsSync(process.argv[2])) {
5 | var configContent = fs.readFileSync(process.argv[2], 'utf8');
6 | var config = yaml.load(configContent);
7 | } else {
8 | console.log('[WARNING] No config file specified, using the default config. In order to specify a config file use: node /path/to/ranaly /path/to/config.yaml');
9 | var config = require('./config_default.yaml');
10 | }
11 |
12 | var isArray = Array.isArray;
13 | var rOnlyLetterOrNumber = /^[a-zA-Z][a-zA-Z0-9 ]*$/;
14 |
15 | function generateID(prefix) {
16 | prefix = prefix || '';
17 | return prefix + (Math.random() * 1000000000 | 0).toString(36);
18 | }
19 |
20 | function toString(value) {
21 | return value ? value.toString() : (value === 0 ? '0' : '');
22 | }
23 |
24 | function initWidgets(wi) {
25 | var widgets = [];
26 | if (isArray(wi)) {
27 | wi.forEach(function (widget) {
28 | if (widget.type === 'custom_code') {
29 | if (widget.file) {
30 | try {
31 | widget.content = fs.readFileSync(widget.file, widget.encoding || 'utf8');
32 | } catch (e) {
33 | console.log('[Error] Couldn\'t load the custom_code widget: ' + e.message);
34 | return;
35 | }
36 | }
37 | if (!widget.content) return;
38 |
39 | } else if (!widget.type || !widget.bucket) {
40 | return;
41 | }
42 | // Every widget should have an ID.
43 | widget.id = generateID('widget_');
44 | // bucket should be an array
45 | if (typeof widget.bucket !== 'undefined' && !isArray(widget.bucket)) {
46 | widget.bucket = [widget.bucket];
47 | }
48 | if (isArray(widget.bucket)) {
49 | widget.bucket = widget.bucket.map(function (b) {
50 | return b.toString();
51 | });
52 | }
53 | // widget should have a default role
54 | widget.role = parseInt(widget.role, 10) || 0;
55 | // bucket should have a title
56 | widget.subtitle = toString(widget.subtitle).replace(/["']/g, '') || '';
57 | widget.y_axis_title = toString(widget.y_axis_title).replace(/["']/g, '') || '';
58 | switch (widget.type) {
59 | case 'amount_line_chart':
60 | widget.title = toString(widget.title) || 'Line Chart';
61 | widget.default_range = toString(widget.default_range) || 'today';
62 | break;
63 | case 'amount_pie_chart':
64 | widget.title = toString(widget.title) || 'Pie Chart';
65 | widget.default_range = toString(widget.default_range) || 'today';
66 | break;
67 | case 'amount_total_count':
68 | widget.title = toString(widget.title) || 'Total Count';
69 | widget.default_range = toString(widget.default_range) || 'all';
70 | break;
71 | case 'amount_today_count':
72 | widget.title = toString(widget.title) || 'Today Count';
73 | break;
74 | case 'realtime_line_chart':
75 | widget.title = toString(widget.title) || 'Realtime Chart';
76 | break;
77 | case 'realtime_count':
78 | widget.title = toString(widget.title) || 'Realtime Count';
79 | break;
80 | case 'datalist_list':
81 | widget.title = toString(widget.title) || 'Data List';
82 | widget.count_per_page = parseInt(widget.count_per_page, 10) || '12';
83 | if (widget.template) {
84 | widget.template = toString(widget.template);
85 | } else {
86 | if (!widget.preset_template) {
87 | widget.preset_template = 'default';
88 | }
89 | var tplPath = __dirname + '/template/' + widget.preset_template + '.mustache';
90 | if (fs.existsSync(tplPath)) {
91 | widget.template = fs.readFileSync(tplPath, 'utf8');
92 | } else {
93 | console.log('[Error] Couldn\'t load datalist widget as the specified preset template isn\'t exists.');
94 | return;
95 | }
96 | }
97 | break;
98 | }
99 | widgets.push(widget);
100 |
101 | });
102 | }
103 | return widgets;
104 | }
105 |
106 | if (!config || typeof config !== 'object') config = {};
107 | config.app_name = toString(config.app_name) || 'New App';
108 | config.redis = config.redis || {};
109 | config.port = config.port || 3000;
110 | config.secret = toString(config.secret) || generateID('cookieSecret');
111 |
112 | // users
113 | var users = [];
114 | if (isArray(config.users)) {
115 | config.users.forEach(function (user) {
116 | if (!user.username) return;
117 | users.push({
118 | username: user.username.toString(),
119 | password: toString(user.password),
120 | role: parseInt(user.role, 10) || 0
121 | });
122 | });
123 | }
124 | config.users = users;
125 |
126 | // pages
127 | var pages = [];
128 | var pageID = {};
129 | if (isArray(config.pages)) {
130 | config.pages.forEach(function (page) {
131 | if (!page.title) return;
132 | page.title = page.title.toString();
133 | // every page should have an id
134 | if (rOnlyLetterOrNumber.test(page.title)) {
135 | page.id = toString(page.id) || page.title.toLowerCase().replace(/ +/g, '-');
136 | }
137 | if (!page.id || pageID[page.id]) {
138 | page.id = generateID('page_');
139 | pageID[page.id] = true;
140 | } else {
141 | pageID[page.id] = true;
142 | }
143 |
144 | page.widgets = initWidgets(page.widgets);
145 |
146 | pages.push(page);
147 | });
148 | }
149 | config.pages = pages;
150 |
151 | module.exports = config;
152 |
--------------------------------------------------------------------------------
/middleware.js:
--------------------------------------------------------------------------------
1 | exports.flash = function (req, res, next) {
2 | var err = req.session.error;
3 | var msg = req.session.info;
4 | delete req.session.error;
5 | delete req.session.info;
6 | res.locals.message = '';
7 | if (err)
8 | res.locals.message = 'Oh snap! ' + err + '
';
9 | if (msg)
10 | res.locals.message = 'Well done! ' + msg + '
';
11 | next();
12 | };
13 |
14 | exports.requireRole = function (req, res, next) {
15 | if (req.session.username) {
16 | res.locals.user = req.session;
17 | next();
18 | } else {
19 | if (req.path !== '/') {
20 | req.session.error = 'You need to sign in before continuing.';
21 | res.redirect('/login?next=' + encodeURIComponent(req.path));
22 | } else {
23 | res.redirect('/login');
24 | }
25 | }
26 | };
27 |
28 | exports.generateMenu = function (config) {
29 | return function (req, res, next) {
30 | res.locals.menus = [];
31 | config.pages.forEach(function (page) {
32 | var pass = false;
33 | page.widgets.forEach(function (widget) {
34 | if (widget.role <= req.session.role) {
35 | pass = true;
36 | }
37 | });
38 | if (pass) {
39 | res.locals.menus.push({
40 | title: page.title,
41 | id: encodeURIComponent(page.id)
42 | });
43 | }
44 | });
45 | next();
46 | };
47 | };
48 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Ranaly",
3 | "version": "0.1.0",
4 | "scripts": {
5 | "start": "node ranaly"
6 | },
7 | "dependencies": {
8 | "express": "3.0.0rc2",
9 | "connect": "*",
10 | "jade": "0.35.0",
11 | "js-yaml": "2.0.2",
12 | "ranaly": "*",
13 | "redis": "*",
14 | "socket.io": "0.9.*",
15 | "session.socket.io": "0.1.4"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/favicon.ico
--------------------------------------------------------------------------------
/public/images/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/bg.jpg
--------------------------------------------------------------------------------
/public/images/col.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/col.gif
--------------------------------------------------------------------------------
/public/images/trend-down.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/trend-down.gif
--------------------------------------------------------------------------------
/public/images/trend-up.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/images/trend-up.gif
--------------------------------------------------------------------------------
/public/img/glyphicons-halflings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/img/glyphicons-halflings-white.png
--------------------------------------------------------------------------------
/public/img/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/img/glyphicons-halflings.png
--------------------------------------------------------------------------------
/public/javascripts/lib/async.js:
--------------------------------------------------------------------------------
1 | (function(){function d(a){var c=!1;return function(){if(c)throw Error("Callback was already called.");c=!0,a.apply(b,arguments)}}var b,c,a={};b=this,null!=b&&(c=b.async),a.noConflict=function(){return b.async=c,a};var e=function(a,b){if(a.forEach)return a.forEach(b);for(var c=0;a.length>c;c+=1)b(a[c],c,a)},f=function(a,b){if(a.map)return a.map(b);var c=[];return e(a,function(a,d,e){c.push(b(a,d,e))}),c},g=function(a,b,c){return a.reduce?a.reduce(b,c):(e(a,function(a,d,e){c=b(c,a,d,e)}),c)},h=function(a){if(Object.keys)return Object.keys(a);var b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c);return b};a.nextTick="undefined"!=typeof process&&process.nextTick?process.nextTick:"function"==typeof setImmediate?function(a){setImmediate(a)}:function(a){setTimeout(a,0)},a.forEach=function(a,b,c){if(c=c||function(){},!a.length)return c();var f=0;e(a,function(e){b(e,d(function(b){b?(c(b),c=function(){}):(f+=1,f>=a.length&&c(null))}))})},a.forEachSeries=function(b,c,d){if(d=d||function(){},!b.length)return d();var e=0,f=function(){var g=!0;c(b[e],function(c){c?(d(c),d=function(){}):(e+=1,e>=b.length?d(null):g?a.nextTick(f):f())}),g=!1};f()},a.forEachLimit=function(a,b,c,d){var e=i(b);e.apply(null,[a,c,d])};var i=function(a){return function(b,c,d){if(d=d||function(){},!b.length||0>=a)return d();var e=0,f=0,g=0;(function h(){if(e>=b.length)return d();for(;a>g&&b.length>f;)f+=1,g+=1,c(b[f-1],function(a){a?(d(a),d=function(){}):(e+=1,g-=1,e>=b.length?d():h())})})()}},j=function(b){return function(){var c=Array.prototype.slice.call(arguments);return b.apply(null,[a.forEach].concat(c))}},k=function(a,b){return function(){var c=Array.prototype.slice.call(arguments);return b.apply(null,[i(a)].concat(c))}},l=function(b){return function(){var c=Array.prototype.slice.call(arguments);return b.apply(null,[a.forEachSeries].concat(c))}},m=function(a,b,c,d){var e=[];b=f(b,function(a,b){return{index:b,value:a}}),a(b,function(a,b){c(a.value,function(c,d){e[a.index]=d,b(c)})},function(a){d(a,e)})};a.map=j(m),a.mapSeries=l(m),a.mapLimit=function(a,b,c,d){return n(b)(a,c,d)};var n=function(a){return k(a,m)};a.reduce=function(b,c,d,e){a.forEachSeries(b,function(a,b){d(c,a,function(a,d){c=d,b(a)})},function(a){e(a,c)})},a.inject=a.reduce,a.foldl=a.reduce,a.reduceRight=function(b,c,d,e){var g=f(b,function(a){return a}).reverse();a.reduce(g,c,d,e)},a.foldr=a.reduceRight;var o=function(a,b,c,d){var e=[];b=f(b,function(a,b){return{index:b,value:a}}),a(b,function(a,b){c(a.value,function(c){c&&e.push(a),b()})},function(){d(f(e.sort(function(a,b){return a.index-b.index}),function(a){return a.value}))})};a.filter=j(o),a.filterSeries=l(o),a.select=a.filter,a.selectSeries=a.filterSeries;var p=function(a,b,c,d){var e=[];b=f(b,function(a,b){return{index:b,value:a}}),a(b,function(a,b){c(a.value,function(c){c||e.push(a),b()})},function(){d(f(e.sort(function(a,b){return a.index-b.index}),function(a){return a.value}))})};a.reject=j(p),a.rejectSeries=l(p);var q=function(a,b,c,d){a(b,function(a,b){c(a,function(c){c?(d(a),d=function(){}):b()})},function(){d()})};a.detect=j(q),a.detectSeries=l(q),a.some=function(b,c,d){a.forEach(b,function(a,b){c(a,function(a){a&&(d(!0),d=function(){}),b()})},function(){d(!1)})},a.any=a.some,a.every=function(b,c,d){a.forEach(b,function(a,b){c(a,function(a){a||(d(!1),d=function(){}),b()})},function(){d(!0)})},a.all=a.every,a.sortBy=function(b,c,d){a.map(b,function(a,b){c(a,function(c,d){c?b(c):b(null,{value:a,criteria:d})})},function(a,b){if(a)return d(a);var c=function(a,b){var c=a.criteria,d=b.criteria;return d>c?-1:c>d?1:0};d(null,f(b.sort(c),function(a){return a.value}))})},a.auto=function(b,c){c=c||function(){};var d=h(b);if(!d.length)return c(null);var f={},i=[],j=function(a){i.unshift(a)},k=function(a){for(var b=0;i.length>b;b+=1)if(i[b]===a)return i.splice(b,1),void 0},l=function(){e(i.slice(0),function(a){a()})};j(function(){h(f).length===d.length&&(c(null,f),c=function(){})}),e(d,function(d){var e=b[d]instanceof Function?[b[d]]:b[d],h=function(b){if(b)c(b),c=function(){};else{var e=Array.prototype.slice.call(arguments,1);1>=e.length&&(e=e[0]),f[d]=e,a.nextTick(l)}},i=e.slice(0,Math.abs(e.length-1))||[],m=function(){return g(i,function(a,b){return a&&f.hasOwnProperty(b)},!0)&&!f.hasOwnProperty(d)};if(m())e[e.length-1](h,f);else{var n=function(){m()&&(k(n),e[e.length-1](h,f))};j(n)}})},a.waterfall=function(b,c){if(c=c||function(){},!b.length)return c();var d=function(b){return function(e){if(e)c.apply(null,arguments),c=function(){};else{var f=Array.prototype.slice.call(arguments,1),g=b.next();g?f.push(d(g)):f.push(c),a.nextTick(function(){b.apply(null,f)})}}};d(a.iterator(b))()};var r=function(a,b,c){if(c=c||function(){},b.constructor===Array)a.map(b,function(a,b){a&&a(function(a){var c=Array.prototype.slice.call(arguments,1);1>=c.length&&(c=c[0]),b.call(null,a,c)})},c);else{var d={};a.forEach(h(b),function(a,c){b[a](function(b){var e=Array.prototype.slice.call(arguments,1);1>=e.length&&(e=e[0]),d[a]=e,c(b)})},function(a){c(a,d)})}};a.parallel=function(b,c){r({map:a.map,forEach:a.forEach},b,c)},a.parallelLimit=function(a,b,c){r({map:n(b),forEach:i(b)},a,c)},a.series=function(b,c){if(c=c||function(){},b.constructor===Array)a.mapSeries(b,function(a,b){a&&a(function(a){var c=Array.prototype.slice.call(arguments,1);1>=c.length&&(c=c[0]),b.call(null,a,c)})},c);else{var d={};a.forEachSeries(h(b),function(a,c){b[a](function(b){var e=Array.prototype.slice.call(arguments,1);1>=e.length&&(e=e[0]),d[a]=e,c(b)})},function(a){c(a,d)})}},a.iterator=function(a){var b=function(c){var d=function(){return a.length&&a[c].apply(null,arguments),d.next()};return d.next=function(){return a.length-1>c?b(c+1):null},d};return b(0)},a.apply=function(a){var b=Array.prototype.slice.call(arguments,1);return function(){return a.apply(null,b.concat(Array.prototype.slice.call(arguments)))}};var s=function(a,b,c,d){var e=[];a(b,function(a,b){c(a,function(a,c){e=e.concat(c||[]),b(a)})},function(a){d(a,e)})};a.concat=j(s),a.concatSeries=l(s),a.whilst=function(b,c,d){if(b()){var e=!0;c(function(f){return f?d(f):(e?a.nextTick(function(){a.whilst(b,c,d)}):a.whilst(b,c,d),void 0)}),e=!1}else d()},a.doWhilst=function(b,c,d){var e=!0;b(function(f){return f?d(f):(c()?e?a.nextTick(function(){a.doWhilst(b,c,d)}):a.doWhilst(b,c,d):d(),void 0)}),e=!1},a.until=function(b,c,d){if(b())d();else{var e=!0;c(function(f){return f?d(f):(e?a.nextTick(function(){a.until(b,c,d)}):a.until(b,c,d),void 0)}),e=!1}},a.doUntil=function(b,c,d){var e=!0;b(function(f){return f?d(f):(c()?d():e?a.nextTick(function(){a.doUntil(b,c,d)}):a.doUntil(b,c,d),void 0)}),e=!1},a.queue=function(b,c){function f(b,d,f,g){d.constructor!==Array&&(d=[d]),e(d,function(d){var e={data:d,callback:"function"==typeof g?g:null};f?b.tasks.unshift(e):b.tasks.push(e),b.saturated&&b.tasks.length===c&&b.saturated(),a.nextTick(b.process)})}var g=0,h={tasks:[],concurrency:c,saturated:null,empty:null,drain:null,push:function(a,b){f(h,a,!1,b)},unshift:function(a,b){f(h,a,!0,b)},process:function(){if(h.concurrency>g&&h.tasks.length){var c=h.tasks.shift();h.empty&&0===h.tasks.length&&h.empty(),g+=1;var e=!0,f=function(){g-=1,c.callback&&c.callback.apply(c,arguments),h.drain&&0===h.tasks.length+g&&h.drain(),h.process()},i=d(function(){var b=arguments;e?a.nextTick(function(){f.apply(null,b)}):f.apply(null,arguments)});b(c.data,i),e=!1}},length:function(){return h.tasks.length},running:function(){return g}};return h},a.cargo=function(b,c){var d=!1,g=[],h={tasks:g,payload:c,saturated:null,empty:null,drain:null,push:function(b,d){b.constructor!==Array&&(b=[b]),e(b,function(a){g.push({data:a,callback:"function"==typeof d?d:null}),h.saturated&&g.length===c&&h.saturated()}),a.nextTick(h.process)},process:function i(){if(!d){if(0===g.length)return h.drain&&h.drain(),void 0;var a="number"==typeof c?g.splice(0,c):g.splice(0),j=f(a,function(a){return a.data});h.empty&&h.empty(),d=!0,b(j,function(){d=!1;var b=arguments;e(a,function(a){a.callback&&a.callback.apply(null,b)}),i()})}},length:function(){return g.length},running:function(){return d}};return h};var t=function(a){return function(b){var c=Array.prototype.slice.call(arguments,1);b.apply(null,c.concat([function(b){var c=Array.prototype.slice.call(arguments,1);"undefined"!=typeof console&&(b?console.error&&console.error(b):console[a]&&e(c,function(b){console[a](b)}))}]))}};a.log=t("log"),a.dir=t("dir"),a.memoize=function(a,b){var c={},d={};b=b||function(a){return a};var e=function(){var e=Array.prototype.slice.call(arguments),f=e.pop(),g=b.apply(null,e);g in c?f.apply(null,c[g]):g in d?d[g].push(f):(d[g]=[f],a.apply(null,e.concat([function(){c[g]=arguments;var a=d[g];delete d[g];for(var b=0,e=a.length;e>b;b++)a[b].apply(null,arguments)}])))};return e.memo=c,e.unmemoized=a,e},a.unmemoize=function(a){return function(){return(a.unmemoized||a).apply(null,arguments)}},a.times=function(b,c,d){for(var e=[],f=0;b>f;f++)e.push(f);return a.map(e,c,d)},a.timesSeries=function(b,c,d){for(var e=[],f=0;b>f;f++)e.push(f);return a.mapSeries(e,c,d)},"undefined"!=typeof define&&define.amd?define([],function(){return a}):"undefined"!=typeof module&&module.exports?module.exports=a:b.async=a})();
--------------------------------------------------------------------------------
/public/javascripts/lib/bootstrap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Bootstrap.js by @fat & @mdo
3 | * plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-alert.js
4 | * Copyright 2012 Twitter, Inc.
5 | * http://www.apache.org/licenses/LICENSE-2.0.txt
6 | */
7 | !function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.focus().trigger("shown")}):b.$element.focus().trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(){var a=this;this.$element.hide(),this.backdrop(function(){a.removeBackdrop(),a.$element.trigger("hidden")})},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?a.proxy(this.$element[0].focus,this.$element[0]):a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!b)return;e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,b):b()):b&&b()}};var c=a.fn.modal;a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a.fn.modal.noConflict=function(){return a.fn.modal=c,this},a(document).on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f).one("hide",function(){c.focus()})})}(window.jQuery),!function(a){function d(){a(b).each(function(){e(a(this)).removeClass("open")})}function e(b){var c=b.attr("data-target"),d;c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,"")),d=c&&a(c);if(!d||!d.length)d=b.parent();return d}var b="[data-toggle=dropdown]",c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),f,g;if(c.is(".disabled, :disabled"))return;return f=e(c),g=f.hasClass("open"),d(),g||f.toggleClass("open"),c.focus(),!1},keydown:function(c){var d,f,g,h,i,j;if(!/(38|40|27)/.test(c.keyCode))return;d=a(this),c.preventDefault(),c.stopPropagation();if(d.is(".disabled, :disabled"))return;h=e(d),i=h.hasClass("open");if(!i||i&&c.keyCode==27)return c.which==27&&h.find(b).focus(),d.click();f=a("[role=menu] li:not(.divider):visible a",h);if(!f.length)return;j=f.index(f.filter(":focus")),c.keyCode==38&&j>0&&j--,c.keyCode==40&&j.*?$/,"").replace(/ /g," ").replace(//g,"").replace(/
/g,'xlink:href="$1"/>').replace(/id=([^" >]+)/g,'id="$1"').replace(/class=([^" ]+)/g,'class="$1"').replace(/ transform /g," ").replace(/:(path|rect)/g,"$1").replace(/style="([^"]+)"/g,function(a){return a.toLowerCase()});d=d.replace(/(url\(#highcharts-[0-9]+)"/g,
16 | "$1").replace(/"/g,"'");d.match(/ xmlns="/g).length===2&&(d=d.replace(/xmlns="[^"]+"/,""));return d},exportChart:function(a,b){var c=this.options.exporting,d=this.getSVG(l(c.chartOptions,b)),a=l(c,a);e.post(a.url,{filename:a.filename||"chart",type:a.type,width:a.width,scale:a.scale||2,svg:d})},print:function(){var a=this,b=a.container,c=[],d=b.parentNode,f=i.body,g=f.childNodes;if(!a.isPrinting)a.isPrinting=!0,k(g,function(a,b){if(a.nodeType===1)c[b]=a.style.display,a.style.display="none"}),
17 | f.appendChild(b),D.print(),setTimeout(function(){d.appendChild(b);k(g,function(a,b){if(a.nodeType===1)a.style.display=c[b]});a.isPrinting=!1},1E3)},contextMenu:function(a,b,c,d,f,g){var e=this,p=e.options.navigation,i=p.menuItemStyle,q=e.chartWidth,r=e.chartHeight,s="cache-"+a,h=e[s],m=C(f,g),x,n,l;if(!h)e[s]=h=j("div",{className:"highcharts-"+a},{position:"absolute",zIndex:1E3,padding:m+"px"},e.container),x=j("div",null,o({MozBoxShadow:"3px 3px 10px #888",WebkitBoxShadow:"3px 3px 10px #888",boxShadow:"3px 3px 10px #888"},
18 | p.menuStyle),h),n=function(){t(h,{display:"none"})},u(h,"mouseleave",function(){l=setTimeout(n,500)}),u(h,"mouseenter",function(){clearTimeout(l)}),k(b,function(a){if(a){var b=j("div",{onmouseover:function(){t(this,p.menuItemHoverStyle)},onmouseout:function(){t(this,i)},innerHTML:a.text||e.options.lang[a.textKey]},o({cursor:"pointer"},i),x);b.onclick=function(){n();a.onclick.apply(e,arguments)};e.exportDivElements.push(b)}}),e.exportDivElements.push(x,h),e.exportMenuWidth=h.offsetWidth,e.exportMenuHeight=
19 | h.offsetHeight;a={display:"block"};c+e.exportMenuWidth>q?a.right=q-c-f-m+"px":a.left=c-m+"px";d+g+e.exportMenuHeight>r?a.bottom=r-d-m+"px":a.top=d+g-m+"px";t(h,a)},addButton:function(a){function b(){r.attr(k);q.attr(m)}var c=this,d=c.renderer,f=l(c.options.navigation.buttonOptions,a),e=f.onclick,i=f.menuItems,p=f.width,j=f.height,q,r,s,h,a=f.borderWidth,m={stroke:f.borderColor},k={stroke:f.symbolStroke,fill:f.symbolFill},n=f.symbolSize||12;if(!c.btnCount)c.btnCount=0;h=c.btnCount++;if(!c.exportDivElements)c.exportDivElements=
20 | [],c.exportSVGElements=[];f.enabled!==!1&&(q=d.rect(0,0,p,j,f.borderRadius,a).align(f,!0).attr(o({fill:f.backgroundColor,"stroke-width":a,zIndex:19},m)).add(),s=d.rect(0,0,p,j,0).align(f).attr({id:f._id,fill:"rgba(255, 255, 255, 0.001)",title:c.options.lang[f._titleKey],zIndex:21}).css({cursor:"pointer"}).on("mouseover",function(){r.attr({stroke:f.hoverSymbolStroke,fill:f.hoverSymbolFill});q.attr({stroke:f.hoverBorderColor})}).on("mouseout",b).on("click",b).add(),i&&(e=function(){b();var a=s.getBBox();
21 | c.contextMenu("menu"+h,i,a.x,a.y,p,j)}),s.on("click",function(){e.apply(c,arguments)}),r=d.symbol(f.symbol,f.symbolX-n/2,f.symbolY-n/2,n,n).align(f,!0).attr(o(k,{"stroke-width":f.symbolStrokeWidth||1,zIndex:20})).add(),c.exportSVGElements.push(q,s,r))},destroyExport:function(){var a,b;for(a=0;a"'\/]/g,function(a){return n[a]})}function p(a){this.string=a,this.tail=a,this.pos=0}function q(a,b){this.view=a,this.parent=b,this._cache={}}function r(){this.clearCache()}function s(b,c,d,e){for(var g,h,i,f="",j=0,k=b.length;k>j;++j)switch(g=b[j],h=g[1],g[0]){case"#":if(i=d.lookup(h),"object"==typeof i)if(l(i))for(var m=0,n=i.length;n>m;++m)f+=s(g[4],c,d.push(i[m]),e);else i&&(f+=s(g[4],c,d.push(i),e));else if("function"==typeof i){var o=null==e?null:e.slice(g[3],g[5]);i=i.call(d.view,o,function(a){return c.render(a,d)}),null!=i&&(f+=i)}else i&&(f+=s(g[4],c,d,e));break;case"^":i=d.lookup(h),(!i||l(i)&&0===i.length)&&(f+=s(g[4],c,d,e));break;case">":i=c.getPartial(h),"function"==typeof i&&(f+=i(d));break;case"&":i=d.lookup(h),null!=i&&(f+=i);break;case"name":i=d.lookup(h),null!=i&&(f+=a.escape(i));break;case"text":f+=h}return f}function t(a){for(var e,b=[],c=b,d=[],f=0,g=a.length;g>f;++f)switch(e=a[f],e[0]){case"#":case"^":d.push(e),c.push(e),c=e[4]=[];break;case"/":var h=d.pop();h[5]=e[2],c=d.length>0?d[d.length-1][4]:b;break;default:c.push(e)}return b}function u(a){for(var c,d,b=[],e=0,f=a.length;f>e;++e)c=a[e],c&&("text"===c[0]&&d&&"text"===d[0]?(d[1]+=c[1],d[3]=c[3]):(d=c,b.push(c)));return b}function v(a){return[RegExp(m(a[0])+"\\s*"),RegExp("\\s*"+m(a[1]))]}var a={};a.name="mustache.js",a.version="0.7.2",a.tags=["{{","}}"],a.Scanner=p,a.Context=q,a.Writer=r;var b=/\s*/,c=/\s+/,d=/\S/,e=/\s*=/,f=/\s*\}/,g=/#|\^|\/|>|\{|&|=|!/,h=RegExp.prototype.test,i=Object.prototype.toString,l=Array.isArray||function(a){return"[object Array]"===i.call(a)},n={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};a.escape=o,p.prototype.eos=function(){return""===this.tail},p.prototype.scan=function(a){var b=this.tail.match(a);return b&&0===b.index?(this.tail=this.tail.substring(b[0].length),this.pos+=b[0].length,b[0]):""},p.prototype.scanUntil=function(a){var b,c=this.tail.search(a);switch(c){case-1:b=this.tail,this.pos+=this.tail.length,this.tail="";break;case 0:b="";break;default:b=this.tail.substring(0,c),this.tail=this.tail.substring(c),this.pos+=c}return b},q.make=function(a){return a instanceof q?a:new q(a)},q.prototype.push=function(a){return new q(a,this)},q.prototype.lookup=function(a){var b=this._cache[a];if(!b){if("."==a)b=this.view;else for(var c=this;c;){if(a.indexOf(".")>0){b=c.view;for(var d=a.split("."),e=0;b&&d.length>e;)b=b[d[e++]]}else b=c.view[a];if(null!=b)break;c=c.parent}this._cache[a]=b}return"function"==typeof b&&(b=b.call(this.view)),b},r.prototype.clearCache=function(){this._cache={},this._partialCache={}},r.prototype.compile=function(b,c){var d=this._cache[b];if(!d){var e=a.parse(b,c);d=this._cache[b]=this.compileTokens(e,b)}return d},r.prototype.compilePartial=function(a,b,c){var d=this.compile(b,c);return this._partialCache[a]=d,d},r.prototype.getPartial=function(a){return a in this._partialCache||!this._loadPartial||this.compilePartial(a,this._loadPartial(a)),this._partialCache[a]},r.prototype.compileTokens=function(a,b){var c=this;return function(d,e){if(e)if("function"==typeof e)c._loadPartial=e;else for(var f in e)c.compilePartial(f,e[f]);return s(a,c,q.make(d),b)}},r.prototype.render=function(a,b,c){return this.compile(a)(b,c)},a.parse=function(d,h){function s(){if(q&&!r)for(;o.length;)delete n[o.pop()];else o=[];q=!1,r=!1}if(d=d||"",h=h||a.tags,"string"==typeof h&&(h=h.split(c)),2!==h.length)throw Error("Invalid tags: "+h.join(", "));for(var w,x,y,z,A,i=v(h),j=new p(d),l=[],n=[],o=[],q=!1,r=!1;!j.eos();){if(w=j.pos,y=j.scanUntil(i[0]))for(var B=0,C=y.length;C>B;++B)z=y.charAt(B),k(z)?o.push(n.length):r=!0,n.push(["text",z,w,w+1]),w+=1,"\n"==z&&s();if(!j.scan(i[0]))break;if(q=!0,x=j.scan(g)||"name",j.scan(b),"="===x?(y=j.scanUntil(e),j.scan(e),j.scanUntil(i[1])):"{"===x?(y=j.scanUntil(RegExp("\\s*"+m("}"+h[1]))),j.scan(f),j.scanUntil(i[1]),x="&"):y=j.scanUntil(i[1]),!j.scan(i[1]))throw Error("Unclosed tag at "+j.pos);if(A=[x,y,w,j.pos],n.push(A),"#"===x||"^"===x)l.push(A);else if("/"===x){if(0===l.length)throw Error('Unopened section "'+y+'" at '+w);var D=l.pop();if(D[1]!==y)throw Error('Unclosed section "'+D[1]+'" at '+w)}else if("name"===x||"{"===x||"&"===x)r=!0;else if("="===x){if(h=y.split(c),2!==h.length)throw Error("Invalid tags at "+w+": "+h.join(", "));i=v(h)}}var D=l.pop();if(D)throw Error('Unclosed section "'+D[1]+'" at '+j.pos);return n=u(n),t(n)};var w=new r;return a.clearCache=function(){return w.clearCache()},a.compile=function(a,b){return w.compile(a,b)},a.compilePartial=function(a,b,c){return w.compilePartial(a,b,c)},a.compileTokens=function(a,b){return w.compileTokens(a,b)},a.render=function(a,b,c){return w.render(a,b,c)},a.to_html=function(b,c,d,e){var f=a.render(b,c,d);return"function"!=typeof e?f:(e(f),void 0)},a}());
--------------------------------------------------------------------------------
/public/javascripts/lib/underscore.js:
--------------------------------------------------------------------------------
1 | (function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this);
2 |
--------------------------------------------------------------------------------
/public/javascripts/ranaly.js:
--------------------------------------------------------------------------------
1 | /*global $:true, Highcharts:true, window:true, Kalendae:true*/
2 | /*jshint unused:false*/
3 | var ranalyApi = {
4 | amount : {
5 | get: function (buckets, dates, callback) {
6 | $.post('/api/amount_get/' + encodeURI(buckets.join(',')), {time: dates}, callback);
7 | },
8 | sum: function (buckets, dates, callback) {
9 | $.post('/api/amount_sum/' + encodeURI(buckets.join(',')), {time: dates}, callback);
10 | }
11 | },
12 | datalist : {
13 | get: function (bucket, from, to, callback) {
14 | $.get('/api/datalist_get/' + encodeURI(bucket), {from: from, to: to}, callback);
15 | }
16 | }
17 | };
18 |
19 | var getRandomInt = function (min, max) {
20 | return Math.floor(Math.random() * (max - min + 1)) + min;
21 | };
22 |
23 | var renderInterval = function (interval, callback) {
24 | setTimeout(function () {
25 | callback();
26 | renderInterval(interval, callback);
27 | }, interval ? interval * 1000 : getRandomInt(20000, 40000));
28 | };
29 |
30 | var startOfDay = function (time) {
31 | return new Date(time.getFullYear(), time.getMonth(), time.getDate());
32 | };
33 |
34 | var CHART_COLORS = [
35 | '#6ea8cd',
36 | '#f97774',
37 | '#92c9a1',
38 | '#e89746',
39 | '#ffe698',
40 | '#ce6faf',
41 | '#8091c8',
42 | '#a47d7c',
43 | '#b5ca92'
44 | ];
45 |
46 | Highcharts.setOptions({
47 | global : {
48 | useUTC : false
49 | },
50 | credits: {
51 | enabled: false
52 | },
53 | colors: CHART_COLORS
54 | });
55 |
56 | window.moment = Kalendae.moment;
57 |
--------------------------------------------------------------------------------
/public/kalendae/arrows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/kalendae/arrows.png
--------------------------------------------------------------------------------
/public/kalendae/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/luin/ranaly/afee3889d3f7ec6666db4b6c83254c46adc1f122/public/kalendae/close.png
--------------------------------------------------------------------------------
/public/kalendae/kalendae.css:
--------------------------------------------------------------------------------
1 | /** Base container **/
2 | .kalendae {
3 | display: inline-block;zoom:1;*display:inline;
4 | background:#eee;
5 | padding:10px;
6 | margin:5px;
7 | border-radius:5px;
8 | -moz-border-radius: 5px;
9 | -webkit-border-radius: 5px;
10 | font-size:11px;
11 | font-family:'Helvetica Neue', 'Helvetica';
12 | cursor:default;
13 | position:relative;
14 | }
15 |
16 | /** Popup Container for Kalendae.Input **/
17 | .kalendae.k-floating {
18 | position:absolute;
19 | top:0;
20 | left:0;
21 | z-index:100000;
22 | margin:0;
23 | box-shadow:0 1px 3px rgba(0,0,0,0.75);
24 | -moz-box-shadow:0 1px 3px rgba(0,0,0,0.75);
25 | -webkit-box-shadow:0 1px 3px rgba(0,0,0,0.75);
26 | }
27 |
28 | /** Kalendae.Input's popup close button **/
29 | .kalendae .k-btn-close {
30 | position:absolute;
31 | top:-8px;
32 | right:-8px;
33 | width:16px;
34 | height:16px;
35 | background:white;
36 | border:2px solid #ccc;
37 | color:#999;
38 | line-height:17px;
39 | text-align:center;
40 | font-size:13px;
41 | border-radius:10px;
42 | box-shadow:0 1px 3px rgba(0,0,0,0.75);
43 | cursor:pointer;
44 | text-decoration:none;
45 | }
46 | .kalendae .k-btn-close:after {content:"\2716";}
47 | .kalendae .k-btn-close:hover {
48 | color:#7EA0E2;
49 | background:white;
50 | border-color:#7EA0E2;
51 | }
52 |
53 | /** Month Container **/
54 | .kalendae .k-calendar {display: inline-block;zoom:1;*display:inline;width:155px;vertical-align:top;}
55 |
56 | /** Month Separator **/
57 | .kalendae .k-separator {display: inline-block;zoom:1;*display:inline;width:2px;vertical-align:top;background:#ddd;height:155px;margin:0px 10px;}
58 |
59 | /** Month Title Row **/
60 | .kalendae .k-title {text-align:center;white-space:nowrap;position:relative;height:18px;}
61 | .kalendae .k-caption {font-size:12px;line-height:18px;}
62 |
63 |
64 | /** Month and Year Buttons **/
65 | .kalendae .k-btn-previous-month,
66 | .kalendae .k-btn-next-month,
67 | .kalendae .k-btn-previous-year,
68 | .kalendae .k-btn-next-year {
69 | width:16px;
70 | height:16px;
71 | cursor:pointer;
72 | position:absolute;
73 | top:-3px;
74 | color:#777;
75 | font-size:26px;
76 | line-height: 18px;
77 | font-weight: bold;
78 | font-family: arial;
79 | text-decoration:none;
80 | }
81 |
82 | .kalendae .k-btn-previous-year {left:0;}
83 | .kalendae .k-btn-previous-month {left:16px;}
84 | .kalendae .k-btn-next-month {right:16px;}
85 | .kalendae .k-btn-next-year {right:0;}
86 |
87 | .kalendae .k-btn-previous-month:after {content:"\2039";}
88 | .kalendae .k-btn-next-month:after {content:"\203A";}
89 |
90 | .kalendae .k-btn-previous-year:after {content:"\00AB";}
91 | .kalendae .k-btn-next-year:after {content:"\00BB";}
92 |
93 | .kalendae .k-btn-previous-month:hover,
94 | .kalendae .k-btn-next-month:hover {color:#7EA0E2;}
95 |
96 | .kalendae .k-btn-previous-year:hover,
97 | .kalendae .k-btn-next-year:hover {color:#6FDF81;}
98 |
99 | /** Remove extra buttons when calendar shows multiple months **/
100 | .kalendae .k-first-month .k-btn-next-month,
101 | .kalendae .k-middle-month .k-btn-next-month,
102 | .kalendae .k-middle-month .k-btn-previous-month,
103 | .kalendae .k-last-month .k-btn-previous-month,
104 | .kalendae .k-first-month .k-btn-next-year,
105 | .kalendae .k-middle-month .k-btn-next-year,
106 | .kalendae .k-middle-month .k-btn-previous-year,
107 | .kalendae .k-last-month .k-btn-previous-year {display:none;}
108 |
109 | /** Disable year nav option **/
110 | .kalendae .k-title.k-disable-year-nav .k-btn-next-year,
111 | .kalendae .k-title.k-disable-year-nav .k-btn-previous-year { display: none; }
112 | .kalendae .k-title.k-disable-year-nav .k-btn-next-month { right: 0; }
113 | .kalendae .k-title.k-disable-year-nav .k-btn-previous-month { left: 0; }
114 |
115 | /** Force specific width for month container contents **/
116 | .kalendae .k-title,
117 | .kalendae .k-header,
118 | .kalendae .k-days {
119 | width:154px;
120 | display:block;
121 | overflow:hidden;
122 | }
123 |
124 |
125 | /** Hide unusable buttons **/
126 | .kalendae.k-disable-next-month-btn .k-btn-next-month,
127 | .kalendae.k-disable-previous-month-btn .k-btn-previous-month,
128 | .kalendae.k-disable-next-year-btn .k-btn-next-year,
129 | .kalendae.k-disable-previous-year-btn .k-btn-previous-year {
130 | display:none;
131 | }
132 |
133 |
134 | /** Week columns and day cells **/
135 | .kalendae .k-header span,
136 | .kalendae .k-days span {
137 | float:left;
138 | margin:1px 1px;
139 | }
140 |
141 | .kalendae .k-header span {
142 | text-align:center;
143 | font-weight:bold;
144 | width:20px;
145 | padding:1px 0;
146 | color:#666;
147 | }
148 |
149 | .kalendae .k-days span {
150 | text-align:right;
151 | width:13px;
152 | height:1.1em;
153 | line-height:1em;
154 | padding:2px 3px 2px 2px;
155 | border:1px solid transparent;
156 | border-radius:3px;
157 | -moz-border-radius: 3px;
158 | -webkit-border-radius: 3px;
159 | color:#999;
160 | }
161 |
162 | /** Today **/
163 | .kalendae .k-today {
164 | text-decoration:underline;
165 | }
166 |
167 | /** Days inside of the month view **/
168 | .kalendae .k-days span.k-in-month.k-active {
169 | border-color:#ddd;
170 | background-color:#fff;
171 | color:#333;
172 | }
173 | /** Days outside of the month view (before the first day of the month, after the last day of the month) **/
174 | .kalendae .k-days span.k-out-of-month {color:#ddd;}
175 |
176 | /** Selectable **/
177 | .kalendae .k-days span.k-active {
178 | cursor:pointer;
179 | }
180 |
181 | /** Selected day, when outside the selectable area **/
182 | .kalendae .k-days span.k-selected {
183 | border-color:#1072A5;
184 | color:#1072A5;
185 | }
186 |
187 | /** Selected day, when inside the selectable area **/
188 | .kalendae .k-days span.k-selected.k-active {
189 | background:#7EA0E2;
190 | color:white;
191 | }
192 |
193 | /** Days between the start and end points on a range, outside of the selectable area **/
194 | .kalendae .k-days span.k-range {
195 | background:none;
196 | border-color:#6DD4FE;
197 | }
198 |
199 | /** Days between the start and end points on a range, inside of the selectable area **/
200 | .kalendae .k-days span.k-range.k-in-month {
201 | background:#C4D4F1;
202 | border-color:#19AEFE;
203 | color:#333;
204 | }
205 |
206 | /** Selectable day, hovered **/
207 | .kalendae .k-days span.k-active:hover {
208 | border-color:#666;
209 | }
210 |
211 |
212 | /*-------------------------------------IE8 ONLY CODE BELOW THIS LINE--------------------------------------------*/
213 |
214 | .kalendae.ie8.k-floating {
215 | border:1px solid #ccc;
216 | }
217 |
218 | .kalendae.ie8 .k-btn-close {
219 | width:20px;
220 | height:20px;
221 | border:none;
222 | background:url('close.png') no-repeat top left;
223 | }
224 | .kalendae.ie8 .k-btn-close:after {display:none;}
225 |
226 | .kalendae.ie8 .k-btn-previous-month,
227 | .kalendae.ie8 .k-btn-next-month,
228 | .kalendae.ie8 .k-btn-previous-year,
229 | .kalendae.ie8 .k-btn-next-year {width:16px;height:16px;cursor:pointer;background:#777 url('arrows.png') no-repeat center left;position:absolute;top:0;}
230 |
231 | .kalendae.ie8 .k-btn-next-month,
232 | .kalendae.ie8 .k-btn-next-year {background-position:center right;}
233 |
234 | .kalendae.ie8 .k-btn-previous-month:hover,
235 | .kalendae.ie8 .k-btn-next-month:hover {background-color:#7EA0E2;}
236 |
237 | .kalendae.ie8 .k-btn-previous-year,
238 | .kalendae.ie8 .k-btn-next-year {background-color:#333;}
239 |
240 | .kalendae.ie8 .k-btn-previous-year:hover,
241 | .kalendae.ie8 .k-btn-next-year:hover {background-color:#6FDF81;}
242 |
243 | .kalendae.ie8 .k-btn-previous-month:after,
244 | .kalendae.ie8 .k-btn-next-month:after,
245 | .kalendae.ie8 .k-btn-previous-year:after,
246 | .kalendae.ie8 .k-btn-next-year:after {display:none;}
247 |
248 |
--------------------------------------------------------------------------------
/public/kalendae/kalendae.js:
--------------------------------------------------------------------------------
1 | /********************************************************************
2 | * Kalendae, a framework agnostic javascript date picker *
3 | * Copyright(c) 2012 Jarvis Badgley (chipersoft@gmail.com) *
4 | * http://github.com/ChiperSoft/Kalendae *
5 | * Version 0.3 *
6 | ********************************************************************/
7 |
8 | (function (undefined) {
9 |
10 | var today;
11 |
12 | var Kalendae = function (targetElement, options) {
13 | if (typeof document.addEventListener !== 'function' && !util.isIE8()) return;
14 |
15 | //if the first argument isn't an element and isn't a string, assume that it is the options object
16 | var is_element = false;
17 | try {
18 | is_element = targetElement instanceof Element;
19 | }
20 | catch (err) {
21 | is_element = !!targetElement && is_element.nodeType === 1;
22 | }
23 | if (!(is_element || typeof(targetElement) === 'string')) options = targetElement;
24 |
25 | var self = this,
26 | classes = self.classes,
27 | opts = self.settings = util.merge(self.defaults, {attachTo:targetElement}, options || {}),
28 | $container = self.container = util.make('div', {'class':classes.container}),
29 | calendars = self.calendars = [],
30 | startDay = moment().day(opts.weekStart),
31 | vsd,
32 | columnHeaders = [],
33 | $cal,
34 | $title,
35 | $caption,
36 | $header,
37 | $days, dayNodes = [],
38 | $span,
39 | i = 0,
40 | j = opts.months;
41 |
42 | if (util.isIE8()) util.addClassName($container, 'ie8');
43 |
44 | //generate the column headers (Su, Mo, Tu, etc)
45 | i = 7;
46 | while (i--) {
47 | columnHeaders.push( startDay.format('ddd').substr(0,opts.columnHeaderLength) );
48 | startDay.add('days',1);
49 | }
50 |
51 | //setup publish/subscribe and apply any subscriptions passed in settings
52 | MinPubSub(self);
53 | if (typeof opts.subscribe === 'object') {
54 | for (i in opts.subscribe) if (opts.subscribe.hasOwnProperty(i)) {
55 | self.subscribe(i, opts.subscribe[i]);
56 | }
57 | }
58 |
59 | //process default selected dates
60 | self._sel = [];
61 | if (!!opts.selected) self.setSelected(opts.selected, false);
62 |
63 | //set the view month
64 | if (!!opts.viewStartDate) {
65 | vsd = moment(opts.viewStartDate, opts.format);
66 | } else if (self._sel.length > 0) {
67 | vsd = moment(self._sel[0]);
68 | } else {
69 | vsd = moment();
70 | }
71 | self.viewStartDate = vsd.date(1);
72 |
73 | var viewDelta = ({
74 | 'past' : opts.months-1,
75 | 'today-past' : opts.months-1,
76 | 'any' : opts.months>2?Math.floor(opts.months/2):0,
77 | 'today-future' : 0,
78 | 'future' : 0
79 | })[this.settings.direction];
80 |
81 |
82 | if (viewDelta && moment().month()==moment(self.viewStartDate).month()){
83 | self.viewStartDate = moment(self.viewStartDate).subtract({M:viewDelta}).date(1);
84 | }
85 |
86 |
87 | if (typeof opts.blackout === 'function') {
88 | self.blackout = opts.blackout;
89 | } else if (!!opts.blackout) {
90 | var bdates = parseDates(opts.blackout, opts.parseSplitDelimiter);
91 | self.blackout = function (input) {
92 | input = moment(input).yearDay();
93 | if (input < 1 || !self._sel || self._sel.length < 1) return false;
94 | var i = bdates.length;
95 | while (i--) if (bdates[i].yearDay() === input) return true;
96 | return false;
97 | }
98 | } else {
99 | self.blackout = function () {return false;}
100 | }
101 |
102 |
103 | self.direction = self.directions[opts.direction] ? self.directions[opts.direction] : self.directions['any'];
104 |
105 |
106 | //for the total months setting, generate N calendar views and add them to the container
107 | j = Math.max(opts.months,1);
108 | while (j--) {
109 | $cal = util.make('div', {'class':classes.calendar}, $container);
110 |
111 | $cal.setAttribute('data-cal-index', j);
112 | if (opts.months > 1) {
113 | if (j == Math.max(opts.months-1,1)) util.addClassName($cal, classes.monthFirst);
114 | else if (j === 0) util.addClassName($cal, classes.monthLast);
115 | else util.addClassName($cal, classes.monthMiddle);
116 | }
117 |
118 | //title bar
119 | $title = util.make('div', {'class':classes.title}, $cal);
120 | if(!opts.useYearNav){
121 | util.addClassName($title, classes.disableYearNav);
122 | }
123 | util.make('a', {'class':classes.previousYear}, $title); //previous button
124 | util.make('a', {'class':classes.previousMonth}, $title); //previous button
125 | util.make('a', {'class':classes.nextYear}, $title); //next button
126 | util.make('a', {'class':classes.nextMonth}, $title); //next button
127 | $caption = util.make('span', {'class':classes.caption}, $title); //title caption
128 |
129 | //column headers
130 | $header = util.make('div', {'class':classes.header}, $cal);
131 | i = 0;
132 | do {
133 | $span = util.make('span', {}, $header);
134 | $span.innerHTML = columnHeaders[i];
135 | } while (++i < 7)
136 |
137 | //individual day cells
138 | $days = util.make('div', {'class':classes.days}, $cal);
139 | i = 0;
140 | dayNodes = [];
141 | while (i++ < 42) {
142 | dayNodes.push(util.make('span', {}, $days));
143 | }
144 |
145 | //store each calendar view for easy redrawing
146 | calendars.push({
147 | caption:$caption,
148 | days:dayNodes
149 | });
150 |
151 | if (j) util.make('div', {'class':classes.monthSeparator}, $container);
152 | }
153 |
154 | self.draw();
155 |
156 | util.addEvent($container, 'mousedown', function (event, target) {
157 | var clickedDate;
158 | if (util.hasClassName(target, classes.nextMonth)) {
159 | //NEXT MONTH BUTTON
160 | if (!self.disableNext && self.publish('view-changed', self, ['next-month']) !== false) {
161 | self.viewStartDate.add('months',1);
162 | self.draw();
163 | }
164 | return false;
165 |
166 | } else if (util.hasClassName(target, classes.previousMonth)) {
167 | //PREVIOUS MONTH BUTTON
168 | if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-month']) !== false) {
169 | self.viewStartDate.subtract('months',1);
170 | self.draw();
171 | }
172 | return false;
173 |
174 | } else if (util.hasClassName(target, classes.nextYear)) {
175 | //NEXT MONTH BUTTON
176 | if (!self.disableNext && self.publish('view-changed', self, ['next-year']) !== false) {
177 | self.viewStartDate.add('years',1);
178 | self.draw();
179 | }
180 | return false;
181 |
182 | } else if (util.hasClassName(target, classes.previousYear)) {
183 | //PREVIOUS MONTH BUTTON
184 | if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-year']) !== false) {
185 | self.viewStartDate.subtract('years',1);
186 | self.draw();
187 | }
188 | return false;
189 |
190 |
191 |
192 | } else if (util.hasClassName(target.parentNode, classes.days) && util.hasClassName(target, classes.dayActive) && (clickedDate = target.getAttribute('data-date'))) {
193 | //DAY CLICK
194 | clickedDate = moment(clickedDate, opts.dayAttributeFormat).hours(12);
195 | if (self.publish('date-clicked', self, [clickedDate]) !== false) {
196 |
197 | switch (opts.mode) {
198 | case 'multiple':
199 | if (!self.addSelected(clickedDate)) self.removeSelected(clickedDate);
200 | break;
201 | case 'range':
202 | self.addSelected(clickedDate);
203 | break;
204 | case 'single':
205 | /* falls through */
206 | default:
207 | self.addSelected(clickedDate);
208 | break;
209 | }
210 |
211 | }
212 | return false;
213 |
214 | }
215 | return false;
216 | });
217 |
218 |
219 | if (!!(opts.attachTo = util.$(opts.attachTo))) {
220 | opts.attachTo.appendChild($container);
221 | }
222 |
223 | };
224 |
225 | Kalendae.prototype = {
226 | defaults : {
227 | attachTo: null, /* the element to attach the root container to. can be string or DOMElement */
228 | months: 1, /* total number of months to display side by side */
229 | weekStart: 0, /* day to use for the start of the week. 0 is Sunday */
230 | direction: 'any', /* past, today-past, any, today-future, future */
231 | directionScrolling: true, /* if a direction other than any is defined, prevent scrolling out of range */
232 | viewStartDate: null, /* date in the month to display. When multiple months, this is the left most */
233 | blackout: null, /* array of dates, or function to be passed a date */
234 | selected: null, /* dates already selected. can be string, date, or array of strings or dates. */
235 | mode: 'single', /* single, multiple, range */
236 | dayOutOfMonthClickable: false,
237 | format: null, /* string used for parsing dates. */
238 | subscribe: null, /* object containing events to subscribe to */
239 |
240 | columnHeaderLength: 2, /* number of characters to show in the column headers */
241 | titleFormat: 'MMMM, YYYY', /* format mask for month titles. See momentjs.com for rules */
242 | dayNumberFormat: 'D', /* format mask for individual days */
243 | dayAttributeFormat: 'YYYY-MM-DD', /* format mask for the data-date attribute set on every span */
244 | parseSplitDelimiter: /,\s*|\s+-\s+/, /* regex to use for splitting multiple dates from a passed string */
245 | rangeDelimiter: ' - ', /* string to use between dates when outputting in range mode */
246 | multipleDelimiter: ', ', /* string to use between dates when outputting in multiple mode */
247 | useYearNav: true,
248 |
249 | dateClassMap: {}
250 | },
251 | classes : {
252 | container :'kalendae',
253 | calendar :'k-calendar',
254 | monthFirst :'k-first-month',
255 | monthMiddle :'k-middle-month',
256 | monthLast :'k-last-month',
257 | title :'k-title',
258 | previousMonth :'k-btn-previous-month',
259 | nextMonth :'k-btn-next-month',
260 | previousYear :'k-btn-previous-year',
261 | nextYear :'k-btn-next-year',
262 | caption :'k-caption',
263 | header :'k-header',
264 | days :'k-days',
265 | dayOutOfMonth :'k-out-of-month',
266 | dayInMonth: 'k-in-month',
267 | dayActive :'k-active',
268 | daySelected :'k-selected',
269 | dayInRange :'k-range',
270 | dayToday :'k-today',
271 | monthSeparator :'k-separator',
272 | disablePreviousMonth :'k-disable-previous-month-btn',
273 | disableNextMonth :'k-disable-next-month-btn',
274 | disablePreviousYear :'k-disable-previous-year-btn',
275 | disableNextYear :'k-disable-next-year-btn',
276 | disableYearNav :'k-disable-year-nav'
277 | },
278 |
279 | disablePreviousMonth: false,
280 | disableNextMonth: false,
281 | disablePreviousYear: false,
282 | disableNextYear: false,
283 |
284 | directions: {
285 | 'past' :function (date) {return moment(date).yearDay() >= today.yearDay();},
286 | 'today-past' :function (date) {return moment(date).yearDay() > today.yearDay();},
287 | 'any' :function (date) {return false;},
288 | 'today-future' :function (date) {return moment(date).yearDay() < today.yearDay();},
289 | 'future' :function (date) {return moment(date).yearDay() <= today.yearDay();}
290 | },
291 |
292 | getSelectedAsDates : function () {
293 | var out = [];
294 | var i=0, c = this._sel.length;
295 | for (;i a && input < b) || (a b)) return -1;
349 | return false;
350 |
351 | case 'multiple':
352 | var i = this._sel.length;
353 | while (i--) {
354 | if (this._sel[i].yearDay() === input) {
355 | return true;
356 | }
357 | }
358 | return false;
359 |
360 |
361 | case 'single':
362 | /* falls through */
363 | default:
364 | return (this._sel[0] && (this._sel[0].yearDay() === input));
365 | }
366 |
367 | return false;
368 | },
369 |
370 | setSelected : function (input, draw) {
371 | this._sel = parseDates(input, this.settings.parseSplitDelimiter, this.settings.format);
372 | this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();});
373 |
374 | if (draw !== false) this.draw();
375 | },
376 |
377 | addSelected : function (date, draw) {
378 | date = moment(date).hours(12);
379 |
380 | if(this.settings.dayOutOfMonthClickable && this.settings.mode !== 'range'){ this.makeSelectedDateVisible(date); }
381 |
382 | switch (this.settings.mode) {
383 | case 'multiple':
384 | if (!this.isSelected(date)) this._sel.push(date);
385 | else return false;
386 | break;
387 | case 'range':
388 |
389 | if (this._sel.length !== 1) this._sel = [date];
390 | else {
391 | if (date.yearDay() > this._sel[0].yearDay()) this._sel[1] = date;
392 | else this._sel = [date, this._sel[0]];
393 | }
394 | break;
395 | case 'single':
396 | /* falls through */
397 | default:
398 | this._sel = [date];
399 | break;
400 | }
401 | this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();});
402 | this.publish('change', this);
403 | if (draw !== false) this.draw();
404 | return true;
405 | },
406 |
407 | makeSelectedDateVisible: function (date) {
408 | outOfViewMonth = moment(date).date('1').diff(this.viewStartDate,'months')
409 |
410 | if(outOfViewMonth < 0){
411 | this.viewStartDate.subtract('months',1);
412 | }
413 | else if(outOfViewMonth > 0 && outOfViewMonth >= this.settings.months){
414 | this.viewStartDate.add('months',1);
415 | }
416 | },
417 |
418 | removeSelected : function (date, draw) {
419 | date = moment(date).yearDay();
420 | var i = this._sel.length;
421 | while (i--) {
422 | if (this._sel[i].yearDay() === date) {
423 | this._sel.splice(i,1);
424 | this.publish('change', this);
425 | if (draw !== false) this.draw();
426 | return true;
427 | }
428 | }
429 | return false;
430 | },
431 |
432 | draw : function draw() {
433 | // return;
434 | var month = moment(this.viewStartDate).hours(12), //force middle of the day to avoid any weird date shifts
435 | day,
436 | classes = this.classes,
437 | cal,
438 | $span,
439 | klass,
440 | i=0, c,
441 | j=0, k,
442 | s,
443 | dateString,
444 | opts = this.settings;
445 |
446 | c = this.calendars.length;
447 |
448 | do {
449 | day = moment(month).date(1);
450 | day.day( day.day() < this.settings.weekStart ? this.settings.weekStart-7 : this.settings.weekStart);
451 | //if the first day of the month is less than our week start, back up a week
452 |
453 | cal = this.calendars[i];
454 | cal.caption.innerHTML = month.format(this.settings.titleFormat);
455 | j = 0;
456 | do {
457 | $span = cal.days[j];
458 |
459 | klass = [];
460 |
461 | s = this.isSelected(day);
462 |
463 | if (s) klass.push(({'-1':classes.dayInRange,'1':classes.daySelected, 'true':classes.daySelected})[s]);
464 |
465 | if (day.month() != month.month()) klass.push(classes.dayOutOfMonth);
466 | else klass.push(classes.dayInMonth);
467 |
468 | if (!(this.blackout(day) || this.direction(day) || (day.month() != month.month() && opts.dayOutOfMonthClickable === false)) || s>0) klass.push(classes.dayActive);
469 |
470 | if (day.yearDay() === today.yearDay()) klass.push(classes.dayToday);
471 |
472 | dateString = day.format(this.settings.dayAttributeFormat);
473 | if (opts.dateClassMap[dateString]) klass.push(opts.dateClassMap[dateString]);
474 |
475 | $span.innerHTML = day.format(opts.dayNumberFormat);
476 | $span.className = klass.join(' ');
477 | $span.setAttribute('data-date', dateString);
478 |
479 |
480 | day.add('days',1);
481 | } while (++j < 42);
482 | month.add('months',1);
483 | } while (++i < c);
484 |
485 | if (opts.directionScrolling) {
486 | var diff = -(moment().diff(month, 'months'));
487 | if (opts.direction==='today-past' || opts.direction==='past') {
488 |
489 | if (diff < 0) {
490 | this.disableNextMonth = false;
491 | util.removeClassName(this.container, classes.disableNextMonth);
492 | } else {
493 | this.disableNextMonth = true;
494 | util.addClassName(this.container, classes.disableNextMonth);
495 | }
496 |
497 | } else if (opts.direction==='today-future' || opts.direction==='future') {
498 |
499 | if (diff >= opts.months) {
500 | this.disablePreviousMonth = false;
501 | util.removeClassName(this.container, classes.disablePreviousMonth);
502 | } else {
503 | this.disablePreviousMonth = true;
504 | util.addClassName(this.container, classes.disablePreviousMonth);
505 | }
506 |
507 | }
508 |
509 |
510 | if (opts.direction==='today-past' || opts.direction==='past') {
511 | if (month.add({y:1}).diff(moment(), 'months') <= 0) {
512 | this.disableNextYear = false;
513 | util.removeClassName(this.container, classes.disableNextYear);
514 | } else {
515 | this.disableNextYear = true;
516 | util.addClassName(this.container, classes.disableNextYear);
517 | }
518 |
519 | } else if (opts.direction==='today-future' || opts.direction==='future') {
520 | if ((month.subtract({y:1}).diff(moment(), 'months') - (opts.months-1)) >= 0) {
521 | this.disablePreviousYear = false;
522 | util.removeClassName(this.container, classes.disablePreviousYear);
523 | } else {
524 | this.disablePreviousYear = true;
525 | util.addClassName(this.container, classes.disablePreviousYear);
526 | }
527 |
528 | }
529 |
530 | }
531 | }
532 | }
533 |
534 | var parseDates = function (input, delimiter, format) {
535 | var output = [];
536 |
537 | if (typeof input === 'string') {
538 | input = input.split(delimiter);
539 | } else if (!util.isArray(input)) {
540 | input = [input];
541 | }
542 |
543 | var c = input.length;
544 | i = 0;
545 | do {
546 | if (input[i]) output.push( moment(input[i], format).hours(12) );
547 | } while (++i < c);
548 |
549 | return output;
550 | }
551 |
552 |
553 |
554 | window.Kalendae = Kalendae;
555 |
556 | var util = Kalendae.util = {
557 |
558 | isIE8: function() {
559 | return !!( (/msie 8./i).test(navigator.appVersion) && !(/opera/i).test(navigator.userAgent) && window.ActiveXObject && XDomainRequest && !window.msPerformance );
560 | },
561 |
562 | // ELEMENT FUNCTIONS
563 |
564 | $: function (elem) {
565 | return (typeof elem == 'string') ? document.getElementById(elem) : elem;
566 | },
567 |
568 | $$: function (selector) {
569 | return document.querySelectorAll(selector);
570 | },
571 |
572 | make: function (tagName, attributes, attach) {
573 | var k, e = document.createElement(tagName);
574 | if (!!attributes) for (k in attributes) if (attributes.hasOwnProperty(k)) e.setAttribute(k, attributes[k]);
575 | if (!!attach) attach.appendChild(e);
576 | return e;
577 | },
578 |
579 | // Returns true if the DOM element is visible, false if it's hidden.
580 | // Checks if display is anything other than none.
581 | isVisible: function (elem) {
582 | // shamelessly copied from jQuery
583 | return elem.offsetWidth > 0 || elem.offsetHeight > 0;
584 | },
585 |
586 | getStyle: function (elem, styleProp) {
587 | var y;
588 | if (elem.currentStyle) {
589 | y = elem.currentStyle[styleProp];
590 | } else if (window.getComputedStyle) {
591 | y = window.getComputedStyle(elem, null)[styleProp];
592 | }
593 | return y;
594 | },
595 |
596 | domReady:function (f){/in/.test(document.readyState) ? setTimeout(function() {util.domReady(f);},9) : f()},
597 |
598 | // Adds a listener callback to a DOM element which is fired on a specified
599 | // event. Callback is sent the event object and the element that triggered the event
600 | addEvent: function (elem, eventName, callback) {
601 | var listener = function (event) {
602 | event = event || window.event;
603 | var target = event.target || event.srcElement;
604 | var block = callback.apply(elem, [event, target]);
605 | if (block === false) {
606 | if (!!event.preventDefault) event.preventDefault();
607 | else {
608 | event.returnValue = false;
609 | event.cancelBubble = true;
610 | }
611 | }
612 | return block;
613 | };
614 | if (elem.attachEvent) { // IE only. The "on" is mandatory.
615 | elem.attachEvent("on" + eventName, listener);
616 | } else { // Other browsers.
617 | elem.addEventListener(eventName, listener, false);
618 | }
619 | return listener;
620 | },
621 |
622 | // Removes a listener callback from a DOM element which is fired on a specified
623 | // event.
624 | removeEvent: function (elem, event, listener) {
625 | if (elem.detachEvent) { // IE only. The "on" is mandatory.
626 | elem.detachEvent("on" + event, listener);
627 | } else { // Other browsers.
628 | elem.removeEventListener(event, listener, false);
629 | }
630 | },
631 |
632 | hasClassName: function(elem, className) { //copied and modified from Prototype.js
633 | if (!(elem = util.$(elem))) return false;
634 | var eClassName = elem.className;
635 | return (eClassName.length > 0 && (eClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(eClassName)));
636 | },
637 |
638 | addClassName: function(elem, className) { //copied and modified from Prototype.js
639 | if (!(elem = util.$(elem))) return;
640 | if (!util.hasClassName(elem, className)) elem.className += (elem.className ? ' ' : '') + className;
641 | },
642 |
643 | removeClassName: function(elem, className) { //copied and modified from Prototype.js
644 | if (!(elem = util.$(elem))) return;
645 | elem.className = util.trimString(elem.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' '));
646 | },
647 |
648 | isFixed: function (elem) {
649 | do {
650 | if (util.getStyle(elem, 'position') === 'fixed') return true;
651 | } while ((elem = elem.offsetParent));
652 | return false;
653 | },
654 |
655 | scrollContainer: function (elem) {
656 | do {
657 | var overflow = util.getStyle(elem, 'overflow');
658 | if (overflow === 'auto' || overflow === 'scroll') return elem;
659 | } while ((elem = elem.parentNode) && elem != window.document.body);
660 | return null;
661 | },
662 |
663 | getPosition: function (elem, isInner) {
664 | var x = elem.offsetLeft,
665 | y = elem.offsetTop,
666 | r = {};
667 |
668 | if (!isInner) {
669 | while ((elem = elem.offsetParent)) {
670 | x += elem.offsetLeft;
671 | y += elem.offsetTop;
672 | }
673 | }
674 |
675 | r[0] = r.left = x;
676 | r[1] = r.top = y;
677 | return r;
678 | },
679 |
680 | getHeight: function (elem) {
681 | return elem.offsetHeight || elem.scrollHeight;
682 | },
683 |
684 | getWidth: function (elem) {
685 | return elem.offsetWidth || elem.scrollWidth;
686 | },
687 |
688 |
689 | // TEXT FUNCTIONS
690 |
691 | trimString: function (input) {
692 | return input.replace(/^\s+/, '').replace(/\s+$/, '');
693 | },
694 |
695 |
696 | // OBJECT FUNCTIONS
697 |
698 | merge: function () {
699 | /* Combines multiple objects into one.
700 | * Syntax: util.extend([true], object1, object2, ... objectN)
701 | * If first argument is true, function will merge recursively.
702 | */
703 |
704 | var deep = (arguments[0]===true),
705 | d = {},
706 | i = deep?1:0;
707 |
708 | var _c = function (a, b) {
709 | if (typeof b !== 'object') return;
710 | for (var k in b) if (b.hasOwnProperty(k)) {
711 | //if property is an object or array, merge the contents instead of overwriting, if extend() was called as such
712 | if (deep && typeof a[k] === 'object' && typeof b[k] === 'object') _update(a[k], b[k]);
713 | else a[k] = b[k];
714 | }
715 | return a;
716 | }
717 |
718 | for (; i < arguments.length; i++) {
719 | _c(d, arguments[i]);
720 | }
721 | return d;
722 | },
723 |
724 | isArray: function (array) {
725 | return !(
726 | !array ||
727 | (!array.length || array.length === 0) ||
728 | typeof array !== 'object' ||
729 | !array.constructor ||
730 | array.nodeType ||
731 | array.item
732 | );
733 | }
734 | };
735 |
736 |
737 | //auto-initializaiton code
738 | if (typeof document.addEventListener === 'function') Kalendae.util.domReady(function () {
739 | var els = util.$$('.auto-kal'),
740 | i = els.length,
741 | e,
742 | options,
743 | optionsRaw;
744 |
745 | while (i--) {
746 | e = els[i];
747 | optionsRaw = e.getAttribute('data-kal');
748 | options = (optionsRaw == null || optionsRaw == "") ? {} : (new Function('return {' + optionsRaw + '};'))();
749 |
750 | if (e.tagName === 'INPUT') {
751 | //if element is an input, bind a popup calendar to the input.
752 | new Kalendae.Input(e, options);
753 | } else {
754 | //otherwise, insert a flat calendar into the element.
755 | new Kalendae(util.merge(options, {attachTo:e}));
756 | }
757 |
758 | }
759 | });
760 | Kalendae.Input = function (targetElement, options) {
761 | if (typeof document.addEventListener !== 'function' && !util.isIE8()) return;
762 |
763 | var $input = this.input = util.$(targetElement),
764 | overwriteInput;
765 |
766 | if (!$input || $input.tagName !== 'INPUT') throw "First argument for Kalendae.Input must be an element or a valid element id.";
767 |
768 | var self = this,
769 | classes = self.classes
770 | opts = self.settings = util.merge(self.defaults, options);
771 |
772 | //force attachment to the body
773 | opts.attachTo = window.document.body;
774 |
775 | //if no override provided, use the input's contents
776 | if (!opts.selected) opts.selected = $input.value;
777 | else overwriteInput = true;
778 |
779 | //call our parent constructor
780 | Kalendae.call(self, opts);
781 |
782 | //create the close button
783 | if (opts.closeButton) {
784 | var $closeButton = util.make('a', {'class':classes.closeButton}, self.container)
785 | util.addEvent($closeButton, 'click', function () {
786 | $input.blur();
787 | });
788 | }
789 |
790 | if (overwriteInput) $input.value = self.getSelected();
791 |
792 | var $container = self.container,
793 | noclose = false;
794 |
795 | $container.style.display = 'none';
796 | util.addClassName($container, classes.positioned);
797 |
798 | util.addEvent($container, 'mousedown', function (event, target) {
799 | noclose = true; //IE8 doesn't obey event blocking when it comes to focusing, so we have to do this shit.
800 | });
801 | util.addEvent(window.document, 'mousedown', function (event, target) {
802 | noclose = false;
803 | });
804 |
805 | util.addEvent($input, 'focus', function () {
806 | self.setSelected(this.value);
807 | self.show();
808 | });
809 |
810 | util.addEvent($input, 'blur', function () {
811 | if (noclose) {
812 | noclose = false;
813 | $input.focus();
814 | }
815 | else self.hide();
816 | });
817 | util.addEvent($input, 'keyup', function (event) {
818 | self.setSelected(this.value);
819 | });
820 |
821 | var $scrollContainer = util.scrollContainer($input);
822 |
823 | if( $scrollContainer ) {
824 |
825 | // Hide calendar when $scrollContainer is scrolled
826 | util.addEvent($scrollContainer, 'scroll', function (event) {
827 | $input.blur();
828 | });
829 | }
830 |
831 | self.subscribe('change', function () {
832 | $input.value = self.getSelected();
833 | });
834 |
835 | };
836 |
837 | Kalendae.Input.prototype = util.merge(Kalendae.prototype, {
838 | defaults : util.merge(Kalendae.prototype.defaults, {
839 | format: 'MM/DD/YYYY',
840 | side: 'bottom',
841 | closeButton: true,
842 | offsetLeft: 0,
843 | offsetTop: 0
844 | }),
845 | classes : util.merge(Kalendae.prototype.classes, {
846 | positioned : 'k-floating',
847 | closeButton: 'k-btn-close'
848 | }),
849 |
850 | show : function () {
851 | var $container = this.container,
852 | style = $container.style,
853 | $input = this.input,
854 | pos = util.getPosition($input),
855 | $scrollContainer = util.scrollContainer($input),
856 | scrollTop = $scrollContainer ? $scrollContainer.scrollTop : 0;
857 |
858 | style.display = '';
859 | switch (opts.side) {
860 | case 'left':
861 | style.left = (pos.left - util.getWidth($container) + this.settings.offsetLeft) + 'px';
862 | style.top = (pos.top + this.settings.offsetTop - scrollTop) + 'px';
863 | break;
864 | case 'right':
865 | style.left = (pos.left + util.getWidth($input)) + 'px';
866 | style.top = (pos.top + this.settings.offsetTop - scrollTop) + 'px';
867 | break;
868 | case 'top':
869 | style.left = (pos.left + this.settings.offsetLeft) + 'px';
870 | style.top = (pos.top - util.getHeight($container) + this.settings.offsetTop - scrollTop) + 'px';
871 | break;
872 | case 'bottom':
873 | /* falls through */
874 | default:
875 | style.left = (pos.left + this.settings.offsetLeft) + 'px';
876 | style.top = (pos.top + util.getHeight($input) + this.settings.offsetTop - scrollTop) + 'px';
877 | break;
878 | }
879 |
880 | style.position = util.isFixed($input) ? 'fixed' : 'absolute';
881 |
882 | this.publish('show', this);
883 | },
884 |
885 | hide : function () {
886 | this.container.style.display = 'none';
887 | this.publish('hide', this);
888 | }
889 |
890 | });
891 |
892 |
893 | /*!
894 | * MinPubSub, modified for use on Kalendae
895 | * Copyright(c) 2011 Daniel Lamb
896 | * https://github.com/daniellmb/MinPubSub
897 | * MIT Licensed
898 | */
899 |
900 | var MinPubSub = function(d){
901 |
902 | if (!d) d = this;
903 |
904 | // the topic/subscription hash
905 | var cache = d.c_ || {}; //check for "c_" cache for unit testing
906 |
907 | d.publish = function(/* String */ topic, /* Object */ target, /* Array? */ args){
908 | // summary:
909 | // Publish some data on a named topic.
910 | // topic: String
911 | // The channel to publish on
912 | // args: Array?
913 | // The data to publish. Each array item is converted into an ordered
914 | // arguments on the subscribed functions.
915 | //
916 | // example:
917 | // Publish stuff on '/some/topic'. Anything subscribed will be called
918 | // with a function signature like: function(a,b,c){ ... }
919 | //
920 | // publish("/some/topic", ["a","b","c"]);
921 |
922 | var subs = cache[topic],
923 | len = subs ? subs.length : 0,
924 | r;
925 |
926 | //can change loop or reverse array if the order matters
927 | while(len--){
928 | r = subs[len].apply(target, args || []);
929 | if (typeof r === 'boolean') return r;
930 | }
931 | };
932 |
933 | d.subscribe = function(/* String */ topic, /* Function */ callback, /* Boolean */ topPriority){
934 | // summary:
935 | // Register a callback on a named topic.
936 | // topic: String
937 | // The channel to subscribe to
938 | // callback: Function
939 | // The handler event. Anytime something is publish'ed on a
940 | // subscribed channel, the callback will be called with the
941 | // published array as ordered arguments.
942 | //
943 | // returns: Array
944 | // A handle which can be used to unsubscribe this particular subscription.
945 | //
946 | // example:
947 | // subscribe("/some/topic", function(a, b, c){ /* handle data */ });
948 |
949 | if(!cache[topic]){
950 | cache[topic] = [];
951 | }
952 | if (topPriority)
953 | cache[topic].push(callback);
954 | else
955 | cache[topic].unshift(callback);
956 | return [topic, callback]; // Array
957 | };
958 |
959 | d.unsubscribe = function(/* Array */ handle){
960 | // summary:
961 | // Disconnect a subscribed function for a topic.
962 | // handle: Array
963 | // The return value from a subscribe call.
964 | // example:
965 | // var handle = subscribe("/some/topic", function(){});
966 | // unsubscribe(handle);
967 |
968 | var subs = cache[handle[0]],
969 | callback = handle[1],
970 | len = subs ? subs.length : 0;
971 |
972 | while(len--){
973 | if(subs[len] === callback){
974 | subs.splice(len, 1);
975 | }
976 | }
977 | };
978 |
979 | };// moment.js
980 | // Altered slightly for use in Kalendae.js
981 | // version : 1.5.0
982 | // author : Tim Wood
983 | // license : MIT
984 | // momentjs.com
985 |
986 | var moment = Kalendae.moment = (function (Date, undefined) {
987 |
988 | var moment,
989 | round = Math.round,
990 | languages = {},
991 | hasModule = (typeof module !== 'undefined'),
992 | paramsToParse = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'),
993 | i,
994 | jsonRegex = /^\/?Date\((\-?\d+)/i,
995 | charactersToReplace = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|zz?|ZZ?|LT|LL?L?L?)/g,
996 | nonuppercaseLetters = /[^A-Z]/g,
997 | timezoneRegex = /\([A-Za-z ]+\)|:[0-9]{2} [A-Z]{3} /g,
998 | tokenCharacters = /(\\)?(MM?M?M?|dd?d?d|DD?D?D?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|ZZ?|T)/g,
999 | inputCharacters = /(\\)?([0-9]+|([a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|([\+\-]\d\d:?\d\d))/gi,
1000 | isoRegex = /\d{4}.\d\d.\d\d(T(\d\d(.\d\d(.\d\d)?)?)?([\+\-]\d\d:?\d\d)?)?/,
1001 | isoFormat = 'YYYY-MM-DDTHH:mm:ssZ',
1002 | isoTimes = [
1003 | ['HH:mm:ss', /T\d\d:\d\d:\d\d/],
1004 | ['HH:mm', /T\d\d:\d\d/],
1005 | ['HH', /T\d\d/]
1006 | ],
1007 | timezoneParseRegex = /([\+\-]|\d\d)/gi,
1008 | VERSION = "1.5.0",
1009 | shortcuts = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|');
1010 |
1011 | // Moment prototype object
1012 | function Moment(date, isUTC) {
1013 | this._d = date;
1014 | this._isUTC = !!isUTC;
1015 | }
1016 |
1017 | // left zero fill a number
1018 | // see http://jsperf.com/left-zero-filling for performance comparison
1019 | function leftZeroFill(number, targetLength) {
1020 | var output = number + '';
1021 | while (output.length < targetLength) {
1022 | output = '0' + output;
1023 | }
1024 | return output;
1025 | }
1026 |
1027 | // helper function for _.addTime and _.subtractTime
1028 | function dateAddRemove(date, _input, adding, val) {
1029 | var isString = (typeof _input === 'string'),
1030 | input = isString ? {} : _input,
1031 | ms, d, M, currentDate;
1032 | if (isString && val) {
1033 | input[_input] = +val;
1034 | }
1035 | ms = (input.ms || input.milliseconds || 0) +
1036 | (input.s || input.seconds || 0) * 1e3 + // 1000
1037 | (input.m || input.minutes || 0) * 6e4 + // 1000 * 60
1038 | (input.h || input.hours || 0) * 36e5; // 1000 * 60 * 60
1039 | d = (input.d || input.days || 0) +
1040 | (input.w || input.weeks || 0) * 7;
1041 | M = (input.M || input.months || 0) +
1042 | (input.y || input.years || 0) * 12;
1043 | if (ms) {
1044 | date.setTime(+date + ms * adding);
1045 | }
1046 | if (d) {
1047 | date.setDate(date.getDate() + d * adding);
1048 | }
1049 | if (M) {
1050 | currentDate = date.getDate();
1051 | date.setDate(1);
1052 | date.setMonth(date.getMonth() + M * adding);
1053 | date.setDate(Math.min(new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(), currentDate));
1054 | }
1055 | return date;
1056 | }
1057 |
1058 | // check if is an array
1059 | function isArray(input) {
1060 | return Object.prototype.toString.call(input) === '[object Array]';
1061 | }
1062 |
1063 | // convert an array to a date.
1064 | // the array should mirror the parameters below
1065 | // note: all values past the year are optional and will default to the lowest possible value.
1066 | // [year, month, day , hour, minute, second, millisecond]
1067 | function dateFromArray(input) {
1068 | return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0);
1069 | }
1070 |
1071 | // format date using native date object
1072 | function formatMoment(m, inputString) {
1073 | var currentMonth = m.month(),
1074 | currentDate = m.date(),
1075 | currentYear = m.year(),
1076 | currentDay = m.day(),
1077 | currentHours = m.hours(),
1078 | currentMinutes = m.minutes(),
1079 | currentSeconds = m.seconds(),
1080 | currentZone = -m.zone(),
1081 | ordinal = moment.ordinal,
1082 | meridiem = moment.meridiem;
1083 | // check if the character is a format
1084 | // return formatted string or non string.
1085 | //
1086 | // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380)
1087 | // for minification and performance
1088 | // see http://jsperf.com/object-of-functions-vs-switch for performance comparison
1089 | function replaceFunction(input) {
1090 | // create a couple variables to be used later inside one of the cases.
1091 | var a, b;
1092 | switch (input) {
1093 | // MONTH
1094 | case 'M' :
1095 | return currentMonth + 1;
1096 | case 'Mo' :
1097 | return (currentMonth + 1) + ordinal(currentMonth + 1);
1098 | case 'MM' :
1099 | return leftZeroFill(currentMonth + 1, 2);
1100 | case 'MMM' :
1101 | return moment.monthsShort[currentMonth];
1102 | case 'MMMM' :
1103 | return moment.months[currentMonth];
1104 | // DAY OF MONTH
1105 | case 'D' :
1106 | return currentDate;
1107 | case 'Do' :
1108 | return currentDate + ordinal(currentDate);
1109 | case 'DD' :
1110 | return leftZeroFill(currentDate, 2);
1111 | // DAY OF YEAR
1112 | case 'DDD' :
1113 | a = new Date(currentYear, currentMonth, currentDate);
1114 | b = new Date(currentYear, 0, 1);
1115 | return ~~ (((a - b) / 864e5) + 1.5);
1116 | case 'DDDo' :
1117 | a = replaceFunction('DDD');
1118 | return a + ordinal(a);
1119 | case 'DDDD' :
1120 | return leftZeroFill(replaceFunction('DDD'), 3);
1121 | // WEEKDAY
1122 | case 'd' :
1123 | return currentDay;
1124 | case 'do' :
1125 | return currentDay + ordinal(currentDay);
1126 | case 'ddd' :
1127 | return moment.weekdaysShort[currentDay];
1128 | case 'dddd' :
1129 | return moment.weekdays[currentDay];
1130 | // WEEK OF YEAR
1131 | case 'w' :
1132 | a = new Date(currentYear, currentMonth, currentDate - currentDay + 5);
1133 | b = new Date(a.getFullYear(), 0, 4);
1134 | return ~~ ((a - b) / 864e5 / 7 + 1.5);
1135 | case 'wo' :
1136 | a = replaceFunction('w');
1137 | return a + ordinal(a);
1138 | case 'ww' :
1139 | return leftZeroFill(replaceFunction('w'), 2);
1140 | // YEAR
1141 | case 'YY' :
1142 | return leftZeroFill(currentYear % 100, 2);
1143 | case 'YYYY' :
1144 | return currentYear;
1145 | // AM / PM
1146 | case 'a' :
1147 | return currentHours > 11 ? meridiem.pm : meridiem.am;
1148 | case 'A' :
1149 | return currentHours > 11 ? meridiem.PM : meridiem.AM;
1150 | // 24 HOUR
1151 | case 'H' :
1152 | return currentHours;
1153 | case 'HH' :
1154 | return leftZeroFill(currentHours, 2);
1155 | // 12 HOUR
1156 | case 'h' :
1157 | return currentHours % 12 || 12;
1158 | case 'hh' :
1159 | return leftZeroFill(currentHours % 12 || 12, 2);
1160 | // MINUTE
1161 | case 'm' :
1162 | return currentMinutes;
1163 | case 'mm' :
1164 | return leftZeroFill(currentMinutes, 2);
1165 | // SECOND
1166 | case 's' :
1167 | return currentSeconds;
1168 | case 'ss' :
1169 | return leftZeroFill(currentSeconds, 2);
1170 | // TIMEZONE
1171 | case 'zz' :
1172 | // depreciating 'zz' fall through to 'z'
1173 | case 'z' :
1174 | return (m._d.toString().match(timezoneRegex) || [''])[0].replace(nonuppercaseLetters, '');
1175 | case 'Z' :
1176 | return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2);
1177 | case 'ZZ' :
1178 | return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4);
1179 | // LONG DATES
1180 | case 'L' :
1181 | case 'LL' :
1182 | case 'LLL' :
1183 | case 'LLLL' :
1184 | case 'LT' :
1185 | return formatMoment(m, moment.longDateFormat[input]);
1186 | // DEFAULT
1187 | default :
1188 | return input.replace(/(^\[)|(\\)|\]$/g, "");
1189 | }
1190 | }
1191 | return inputString.replace(charactersToReplace, replaceFunction);
1192 | }
1193 |
1194 | // date from string and format string
1195 | function makeDateFromStringAndFormat(string, format) {
1196 | var inArray = [0, 0, 1, 0, 0, 0, 0],
1197 | timezoneHours = 0,
1198 | timezoneMinutes = 0,
1199 | isUsingUTC = false,
1200 | inputParts = string.match(inputCharacters),
1201 | formatParts = format.match(tokenCharacters),
1202 | len = Math.min(inputParts.length, formatParts.length),
1203 | i,
1204 | isPm;
1205 |
1206 | // function to convert string input to date
1207 | function addTime(format, input) {
1208 | var a;
1209 | switch (format) {
1210 | // MONTH
1211 | case 'M' :
1212 | // fall through to MM
1213 | case 'MM' :
1214 | inArray[1] = ~~input - 1;
1215 | break;
1216 | case 'MMM' :
1217 | // fall through to MMMM
1218 | case 'MMMM' :
1219 | for (a = 0; a < 12; a++) {
1220 | if (moment.monthsParse[a].test(input)) {
1221 | inArray[1] = a;
1222 | break;
1223 | }
1224 | }
1225 | break;
1226 | // DAY OF MONTH
1227 | case 'D' :
1228 | // fall through to DDDD
1229 | case 'DD' :
1230 | // fall through to DDDD
1231 | case 'DDD' :
1232 | // fall through to DDDD
1233 | case 'DDDD' :
1234 | inArray[2] = ~~input;
1235 | break;
1236 | // YEAR
1237 | case 'YY' :
1238 | input = ~~input;
1239 | inArray[0] = input + (input > 70 ? 1900 : 2000);
1240 | break;
1241 | case 'YYYY' :
1242 | inArray[0] = ~~Math.abs(input);
1243 | break;
1244 | // AM / PM
1245 | case 'a' :
1246 | // fall through to A
1247 | case 'A' :
1248 | isPm = (input.toLowerCase() === 'pm');
1249 | break;
1250 | // 24 HOUR
1251 | case 'H' :
1252 | // fall through to hh
1253 | case 'HH' :
1254 | // fall through to hh
1255 | case 'h' :
1256 | // fall through to hh
1257 | case 'hh' :
1258 | inArray[3] = ~~input;
1259 | break;
1260 | // MINUTE
1261 | case 'm' :
1262 | // fall through to mm
1263 | case 'mm' :
1264 | inArray[4] = ~~input;
1265 | break;
1266 | // SECOND
1267 | case 's' :
1268 | // fall through to ss
1269 | case 'ss' :
1270 | inArray[5] = ~~input;
1271 | break;
1272 | // TIMEZONE
1273 | case 'Z' :
1274 | // fall through to ZZ
1275 | case 'ZZ' :
1276 | isUsingUTC = true;
1277 | a = (input || '').match(timezoneParseRegex);
1278 | if (a && a[1]) {
1279 | timezoneHours = ~~a[1];
1280 | }
1281 | if (a && a[2]) {
1282 | timezoneMinutes = ~~a[2];
1283 | }
1284 | // reverse offsets
1285 | if (a && a[0] === '+') {
1286 | timezoneHours = -timezoneHours;
1287 | timezoneMinutes = -timezoneMinutes;
1288 | }
1289 | break;
1290 | }
1291 | }
1292 | for (i = 0; i < len; i++) {
1293 | addTime(formatParts[i], inputParts[i]);
1294 | }
1295 | // handle am pm
1296 | if (isPm && inArray[3] < 12) {
1297 | inArray[3] += 12;
1298 | }
1299 | // if is 12 am, change hours to 0
1300 | if (isPm === false && inArray[3] === 12) {
1301 | inArray[3] = 0;
1302 | }
1303 | // handle timezone
1304 | inArray[3] += timezoneHours;
1305 | inArray[4] += timezoneMinutes;
1306 | // return
1307 | return isUsingUTC ? new Date(Date.UTC.apply({}, inArray)) : dateFromArray(inArray);
1308 | }
1309 |
1310 | // compare two arrays, return the number of differences
1311 | function compareArrays(array1, array2) {
1312 | var len = Math.min(array1.length, array2.length),
1313 | lengthDiff = Math.abs(array1.length - array2.length),
1314 | diffs = 0,
1315 | i;
1316 | for (i = 0; i < len; i++) {
1317 | if (~~array1[i] !== ~~array2[i]) {
1318 | diffs++;
1319 | }
1320 | }
1321 | return diffs + lengthDiff;
1322 | }
1323 |
1324 | // date from string and array of format strings
1325 | function makeDateFromStringAndArray(string, formats) {
1326 | var output,
1327 | inputParts = string.match(inputCharacters),
1328 | scores = [],
1329 | scoreToBeat = 99,
1330 | i,
1331 | curDate,
1332 | curScore;
1333 | for (i = 0; i < formats.length; i++) {
1334 | curDate = makeDateFromStringAndFormat(string, formats[i]);
1335 | curScore = compareArrays(inputParts, formatMoment(new Moment(curDate), formats[i]).match(inputCharacters));
1336 | if (curScore < scoreToBeat) {
1337 | scoreToBeat = curScore;
1338 | output = curDate;
1339 | }
1340 | }
1341 | return output;
1342 | }
1343 |
1344 | // date from iso format
1345 | function makeDateFromString(string) {
1346 | var format = 'YYYY-MM-DDT',
1347 | i;
1348 | if (isoRegex.exec(string)) {
1349 | for (i = 0; i < 3; i++) {
1350 | if (isoTimes[i][1].exec(string)) {
1351 | format += isoTimes[i][0];
1352 | break;
1353 | }
1354 | }
1355 | return makeDateFromStringAndFormat(string, format + 'Z');
1356 | }
1357 | return new Date(string);
1358 | }
1359 |
1360 | // helper function for _date.from() and _date.fromNow()
1361 | function substituteTimeAgo(string, number, withoutSuffix) {
1362 | var rt = moment.relativeTime[string];
1363 | return (typeof rt === 'function') ?
1364 | rt(number || 1, !!withoutSuffix, string) :
1365 | rt.replace(/%d/i, number || 1);
1366 | }
1367 |
1368 | function relativeTime(milliseconds, withoutSuffix) {
1369 | var seconds = round(Math.abs(milliseconds) / 1000),
1370 | minutes = round(seconds / 60),
1371 | hours = round(minutes / 60),
1372 | days = round(hours / 24),
1373 | years = round(days / 365),
1374 | args = seconds < 45 && ['s', seconds] ||
1375 | minutes === 1 && ['m'] ||
1376 | minutes < 45 && ['mm', minutes] ||
1377 | hours === 1 && ['h'] ||
1378 | hours < 22 && ['hh', hours] ||
1379 | days === 1 && ['d'] ||
1380 | days <= 25 && ['dd', days] ||
1381 | days <= 45 && ['M'] ||
1382 | days < 345 && ['MM', round(days / 30)] ||
1383 | years === 1 && ['y'] || ['yy', years];
1384 | args[2] = withoutSuffix;
1385 | return substituteTimeAgo.apply({}, args);
1386 | }
1387 |
1388 | moment = function (input, format) {
1389 | if (input === null || input === '') {
1390 | return null;
1391 | }
1392 | var date,
1393 | matched;
1394 | // parse Moment object
1395 | if (input && input._d instanceof Date) {
1396 | date = new Date(+input._d);
1397 | // parse string and format
1398 | } else if (format) {
1399 | if (isArray(format)) {
1400 | date = makeDateFromStringAndArray(input, format);
1401 | } else {
1402 | date = makeDateFromStringAndFormat(input, format);
1403 | }
1404 | // evaluate it as a JSON-encoded date
1405 | } else {
1406 | matched = jsonRegex.exec(input);
1407 | date = input === undefined ? new Date() :
1408 | matched ? new Date(+matched[1]) :
1409 | input instanceof Date ? input :
1410 | isArray(input) ? dateFromArray(input) :
1411 | typeof input === 'string' ? makeDateFromString(input) :
1412 | new Date(input);
1413 | }
1414 | return new Moment(date);
1415 | };
1416 |
1417 | // creating with utc
1418 | moment.utc = function (input, format) {
1419 | if (isArray(input)) {
1420 | return new Moment(new Date(Date.UTC.apply({}, input)), true);
1421 | }
1422 | return (format && input) ? moment(input + ' 0', format + ' Z').utc() : moment(input).utc();
1423 | };
1424 |
1425 | // humanizeDuration
1426 | moment.humanizeDuration = function (num, type, withSuffix) {
1427 | var difference = +num,
1428 | rel = moment.relativeTime,
1429 | output;
1430 | switch (type) {
1431 | case "seconds" :
1432 | difference *= 1000; // 1000
1433 | break;
1434 | case "minutes" :
1435 | difference *= 60000; // 60 * 1000
1436 | break;
1437 | case "hours" :
1438 | difference *= 3600000; // 60 * 60 * 1000
1439 | break;
1440 | case "days" :
1441 | difference *= 86400000; // 24 * 60 * 60 * 1000
1442 | break;
1443 | case "weeks" :
1444 | difference *= 604800000; // 7 * 24 * 60 * 60 * 1000
1445 | break;
1446 | case "months" :
1447 | difference *= 2592000000; // 30 * 24 * 60 * 60 * 1000
1448 | break;
1449 | case "years" :
1450 | difference *= 31536000000; // 365 * 24 * 60 * 60 * 1000
1451 | break;
1452 | default :
1453 | withSuffix = !!type;
1454 | break;
1455 | }
1456 | output = relativeTime(difference, !withSuffix);
1457 | return withSuffix ? (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output) : output;
1458 | };
1459 |
1460 | // version number
1461 | moment.version = VERSION;
1462 |
1463 | // default format
1464 | moment.defaultFormat = isoFormat;
1465 |
1466 | // language switching and caching
1467 | moment.lang = function (key, values) {
1468 | var i,
1469 | param,
1470 | req,
1471 | parse = [];
1472 | if (values) {
1473 | for (i = 0; i < 12; i++) {
1474 | parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i');
1475 | }
1476 | values.monthsParse = values.monthsParse || parse;
1477 | languages[key] = values;
1478 | }
1479 | if (languages[key]) {
1480 | for (i = 0; i < paramsToParse.length; i++) {
1481 | param = paramsToParse[i];
1482 | moment[param] = languages[key][param] || moment[param];
1483 | }
1484 | } else {
1485 | if (hasModule) {
1486 | req = require('./lang/' + key);
1487 | moment.lang(key, req);
1488 | }
1489 | }
1490 | };
1491 |
1492 | // set default language
1493 | moment.lang('en', {
1494 | months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"),
1495 | monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),
1496 | weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),
1497 | weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),
1498 | longDateFormat : {
1499 | LT : "h:mm A",
1500 | L : "MM/DD/YYYY",
1501 | LL : "MMMM D YYYY",
1502 | LLL : "MMMM D YYYY LT",
1503 | LLLL : "dddd, MMMM D YYYY LT"
1504 | },
1505 | meridiem : {
1506 | AM : 'AM',
1507 | am : 'am',
1508 | PM : 'PM',
1509 | pm : 'pm'
1510 | },
1511 | calendar : {
1512 | sameDay : '[Today at] LT',
1513 | nextDay : '[Tomorrow at] LT',
1514 | nextWeek : 'dddd [at] LT',
1515 | lastDay : '[Yesterday at] LT',
1516 | lastWeek : '[last] dddd [at] LT',
1517 | sameElse : 'L'
1518 | },
1519 | relativeTime : {
1520 | future : "in %s",
1521 | past : "%s ago",
1522 | s : "a few seconds",
1523 | m : "a minute",
1524 | mm : "%d minutes",
1525 | h : "an hour",
1526 | hh : "%d hours",
1527 | d : "a day",
1528 | dd : "%d days",
1529 | M : "a month",
1530 | MM : "%d months",
1531 | y : "a year",
1532 | yy : "%d years"
1533 | },
1534 | ordinal : function (number) {
1535 | var b = number % 10;
1536 | return (~~ (number % 100 / 10) === 1) ? 'th' :
1537 | (b === 1) ? 'st' :
1538 | (b === 2) ? 'nd' :
1539 | (b === 3) ? 'rd' : 'th';
1540 | }
1541 | });
1542 |
1543 | // compare moment object
1544 | moment.isMoment = function (obj) {
1545 | return obj instanceof Moment;
1546 | };
1547 |
1548 | // shortcut for prototype
1549 | moment.fn = Moment.prototype = {
1550 |
1551 | clone : function () {
1552 | return moment(this);
1553 | },
1554 |
1555 | valueOf : function () {
1556 | return +this._d;
1557 | },
1558 |
1559 | 'native' : function () {
1560 | return this._d;
1561 | },
1562 |
1563 | toString : function () {
1564 | return this._d.toString();
1565 | },
1566 |
1567 | toDate : function () {
1568 | return this._d;
1569 | },
1570 |
1571 | utc : function () {
1572 | this._isUTC = true;
1573 | return this;
1574 | },
1575 |
1576 | local : function () {
1577 | this._isUTC = false;
1578 | return this;
1579 | },
1580 |
1581 | format : function (inputString) {
1582 | return formatMoment(this, inputString ? inputString : moment.defaultFormat);
1583 | },
1584 |
1585 | add : function (input, val) {
1586 | this._d = dateAddRemove(this._d, input, 1, val);
1587 | return this;
1588 | },
1589 |
1590 | subtract : function (input, val) {
1591 | this._d = dateAddRemove(this._d, input, -1, val);
1592 | return this;
1593 | },
1594 |
1595 | diff : function (input, val, asFloat) {
1596 | var inputMoment = moment(input),
1597 | zoneDiff = (this.zone() - inputMoment.zone()) * 6e4,
1598 | diff = this._d - inputMoment._d - zoneDiff,
1599 | year = this.year() - inputMoment.year(),
1600 | month = this.month() - inputMoment.month(),
1601 | date = this.date() - inputMoment.date(),
1602 | output;
1603 | if (val === 'months') {
1604 | output = year * 12 + month + date / 30;
1605 | } else if (val === 'years') {
1606 | output = year + month / 12;
1607 | } else {
1608 | output = val === 'seconds' ? diff / 1e3 : // 1000
1609 | val === 'minutes' ? diff / 6e4 : // 1000 * 60
1610 | val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60
1611 | val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24
1612 | val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7
1613 | diff;
1614 | }
1615 | return asFloat ? output : round(output);
1616 | },
1617 |
1618 | from : function (time, withoutSuffix) {
1619 | return moment.humanizeDuration(this.diff(time), !withoutSuffix);
1620 | },
1621 |
1622 | fromNow : function (withoutSuffix) {
1623 | return this.from(moment(), withoutSuffix);
1624 | },
1625 |
1626 | calendar : function () {
1627 | var diff = this.diff(moment().sod(), 'days', true),
1628 | calendar = moment.calendar,
1629 | allElse = calendar.sameElse,
1630 | format = diff < -6 ? allElse :
1631 | diff < -1 ? calendar.lastWeek :
1632 | diff < 0 ? calendar.lastDay :
1633 | diff < 1 ? calendar.sameDay :
1634 | diff < 2 ? calendar.nextDay :
1635 | diff < 7 ? calendar.nextWeek : allElse;
1636 | return this.format(typeof format === 'function' ? format.apply(this) : format);
1637 | },
1638 |
1639 | isLeapYear : function () {
1640 | var year = this.year();
1641 | return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
1642 | },
1643 |
1644 | isDST : function () {
1645 | return (this.zone() < moment([this.year()]).zone() ||
1646 | this.zone() < moment([this.year(), 5]).zone());
1647 | },
1648 |
1649 | day : function (input) {
1650 | var day = this._d.getDay();
1651 | return (typeof input === 'undefined') ? day :
1652 | this.add({ d : input - day });
1653 | },
1654 |
1655 | sod: function () {
1656 | return this.clone()
1657 | .hours(0)
1658 | .minutes(0)
1659 | .seconds(0)
1660 | .milliseconds(0);
1661 | },
1662 |
1663 | eod: function () {
1664 | // end of day = start of day plus 1 day, minus 1 millisecond
1665 | return this.sod().add({
1666 | d : 1,
1667 | ms : -1
1668 | });
1669 | },
1670 |
1671 | zone : function () {
1672 | return this._isUTC ? 0 : this._d.getTimezoneOffset();
1673 | },
1674 |
1675 | daysInMonth : function () {
1676 | return this.clone().month(this.month() + 1).date(0).date();
1677 | }
1678 | };
1679 |
1680 | // helper for adding shortcuts
1681 | function makeShortcut(name, key) {
1682 | moment.fn[name] = function (input) {
1683 | var utc = this._isUTC ? 'UTC' : '';
1684 | if (typeof input !== 'undefined') {
1685 | this._d['set' + utc + key](input);
1686 | return this;
1687 | } else {
1688 | return this._d['get' + utc + key]();
1689 | }
1690 | };
1691 | }
1692 |
1693 | // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds)
1694 | for (i = 0; i < shortcuts.length; i ++) {
1695 | makeShortcut(shortcuts[i].toLowerCase(), shortcuts[i]);
1696 | }
1697 |
1698 | // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear')
1699 | makeShortcut('year', 'FullYear');
1700 |
1701 | return moment;
1702 | })(Date);
1703 |
1704 | //function to reset the date object to 00:00 GMT
1705 | moment.fn.stripTime = function () {
1706 | this._d = new Date(Math.floor(this._d.valueOf() / 86400000) * 86400000);
1707 | return this;
1708 | }
1709 |
1710 |
1711 | //function to get the total number of days since the epoch.
1712 | moment.fn.yearDay = function (input) {
1713 | var yearday = Math.floor(this._d / 86400000);
1714 | return (typeof input === 'undefined') ? yearday :
1715 | this.add({ d : input - yearday });
1716 | }
1717 |
1718 | today = moment().stripTime();
1719 |
1720 | if (typeof jQuery !== 'undefined' && typeof document.addEventListener === 'function') {
1721 | jQuery.fn.kalendae = function (options) {
1722 | this.each(function (i, e) {
1723 | if (e.tagName === 'INPUT') {
1724 | //if element is an input, bind a popup calendar to the input.
1725 | $(e).data('kalendae', new Kalendae.Input(e, options));
1726 | } else {
1727 | //otherwise, insert a flat calendar into the element.
1728 | $(e).data('kalendae', new Kalendae($.extend({}, {attachTo:e}, options)));
1729 | }
1730 | });
1731 | return this;
1732 | }
1733 | }
1734 |
1735 |
1736 | })();
1737 |
--------------------------------------------------------------------------------
/public/kalendae/kalendae.min.js:
--------------------------------------------------------------------------------
1 | /********************************************************************
2 | * Kalendae, a framework agnostic javascript date picker *
3 | * Copyright(c) 2012 Jarvis Badgley (chipersoft@gmail.com) *
4 | * http://github.com/ChiperSoft/Kalendae *
5 | * Version 0.3 *
6 | ********************************************************************/
7 |
8 | (function(){var t,q=function(a,b){if("function"===typeof document.addEventListener||f.isIE8()){var c=!1;try{c=a instanceof Element}catch(e){c=!!a&&1===c.nodeType}c||"string"===typeof a||(b=a);var d=this,g=d.classes,l=d.settings=f.merge(d.defaults,{attachTo:a},b||{}),c=d.container=f.make("div",{"class":g.container}),h=d.calendars=[],w=j().day(l.weekStart),n,o=[],m,k,x;m=[];var p;k=0;n=l.months;f.isIE8()&&f.addClassName(c,"ie8");for(k=7;k--;)o.push(w.format("ddd").substr(0,l.columnHeaderLength)),w.add("days",
9 | 1);K(d);if("object"===typeof l.subscribe)for(k in l.subscribe)l.subscribe.hasOwnProperty(k)&&d.subscribe(k,l.subscribe[k]);d._sel=[];l.selected&&d.setSelected(l.selected,!1);n=l.viewStartDate?j(l.viewStartDate,l.format):0a||!d._sel||1>d._sel.length)return!1;for(var c=q.length;c--;)if(q[c].yearDay()===a)return!0;return!1}}else d.blackout=function(){return!1};d.direction=d.directions[l.direction]?d.directions[l.direction]:d.directions.any;for(n=Math.max(l.months,1);n--;){m=f.make("div",{"class":g.calendar},c);m.setAttribute("data-cal-index",n);1++k);x=f.make("div",
12 | {"class":g.days},m);k=0;for(m=[];42>k++;)m.push(f.make("span",{},x));h.push({caption:w,days:m});n&&f.make("div",{"class":g.monthSeparator},c)}d.draw();f.addEvent(c,"mousedown",function(a,c){var b;if(f.hasClassName(c,g.nextMonth))!d.disableNext&&!1!==d.publish("view-changed",d,["next-month"])&&(d.viewStartDate.add("months",1),d.draw());else if(f.hasClassName(c,g.previousMonth))!d.disablePreviousMonth&&!1!==d.publish("view-changed",d,["previous-month"])&&(d.viewStartDate.subtract("months",1),d.draw());
13 | else if(f.hasClassName(c,g.nextYear))!d.disableNext&&!1!==d.publish("view-changed",d,["next-year"])&&(d.viewStartDate.add("years",1),d.draw());else if(f.hasClassName(c,g.previousYear))!d.disablePreviousMonth&&!1!==d.publish("view-changed",d,["previous-year"])&&(d.viewStartDate.subtract("years",1),d.draw());else if(f.hasClassName(c.parentNode,g.days)&&f.hasClassName(c,g.dayActive)&&(b=c.getAttribute("data-date")))if(b=j(b,l.dayAttributeFormat).hours(12),!1!==d.publish("date-clicked",d,[b]))switch(l.mode){case "multiple":d.addSelected(b)||
14 | d.removeSelected(b);break;case "range":d.addSelected(b);break;default:d.addSelected(b)}return!1});(l.attachTo=f.$(l.attachTo))&&l.attachTo.appendChild(c)}};q.prototype={defaults:{attachTo:null,months:1,weekStart:0,direction:"any",directionScrolling:!0,viewStartDate:null,blackout:null,selected:null,mode:"single",dayOutOfMonthClickable:!1,format:null,subscribe:null,columnHeaderLength:2,titleFormat:"MMMM, YYYY",dayNumberFormat:"D",dayAttributeFormat:"YYYY-MM-DD",parseSplitDelimiter:/,\s*|\s+-\s+/,rangeDelimiter:" - ",
15 | multipleDelimiter:", ",useYearNav:!0,dateClassMap:{}},classes:{container:"kalendae",calendar:"k-calendar",monthFirst:"k-first-month",monthMiddle:"k-middle-month",monthLast:"k-last-month",title:"k-title",previousMonth:"k-btn-previous-month",nextMonth:"k-btn-next-month",previousYear:"k-btn-previous-year",nextYear:"k-btn-next-year",caption:"k-caption",header:"k-header",days:"k-days",dayOutOfMonth:"k-out-of-month",dayInMonth:"k-in-month",dayActive:"k-active",daySelected:"k-selected",dayInRange:"k-range",
16 | dayToday:"k-today",monthSeparator:"k-separator",disablePreviousMonth:"k-disable-previous-month-btn",disableNextMonth:"k-disable-next-month-btn",disablePreviousYear:"k-disable-previous-year-btn",disableNextYear:"k-disable-next-year-btn",disableYearNav:"k-disable-year-nav"},disablePreviousMonth:!1,disableNextMonth:!1,disablePreviousYear:!1,disableNextYear:!1,directions:{past:function(a){return j(a).yearDay()>=t.yearDay()},"today-past":function(a){return j(a).yearDay()>t.yearDay()},any:function(){return!1},
17 | "today-future":function(a){return j(a).yearDay()a||!this._sel||1>this._sel.length)return!1;switch(this.settings.mode){case "range":var b=this._sel[0]?this._sel[0].yearDay():0,c=this._sel[1]?this._sel[1].yearDay():0;return b===a||c===a?1:!b||!c?0:a>b&&ac?-1:!1;case "multiple":for(b=
19 | this._sel.length;b--;)if(this._sel[b].yearDay()===a)return!0;return!1;default:return this._sel[0]&&this._sel[0].yearDay()===a}},setSelected:function(a,b){this._sel=E(a,this.settings.parseSplitDelimiter,this.settings.format);this._sel.sort(function(a,b){return a.yearDay()-b.yearDay()});!1!==b&&this.draw()},addSelected:function(a,b){a=j(a).hours(12);this.settings.dayOutOfMonthClickable&&"range"!==this.settings.mode&&this.makeSelectedDateVisible(a);switch(this.settings.mode){case "multiple":if(this.isSelected(a))return!1;
20 | this._sel.push(a);break;case "range":1!==this._sel.length?this._sel=[a]:a.yearDay()>this._sel[0].yearDay()?this._sel[1]=a:this._sel=[a,this._sel[0]];break;default:this._sel=[a]}this._sel.sort(function(a,b){return a.yearDay()-b.yearDay()});this.publish("change",this);!1!==b&&this.draw();return!0},makeSelectedDateVisible:function(a){outOfViewMonth=j(a).date("1").diff(this.viewStartDate,"months");0>outOfViewMonth?this.viewStartDate.subtract("months",1):0=this.settings.months&&
21 | this.viewStartDate.add("months",1)},removeSelected:function(a,b){for(var a=j(a).yearDay(),c=this._sel.length;c--;)if(this._sel[c].yearDay()===a)return this._sel.splice(c,1),this.publish("change",this),!1!==b&&this.draw(),!0;return!1},draw:function(){var a=j(this.viewStartDate).hours(12),b,c=this.classes,e,d,g,l=0,h,p=0,n,o=this.settings;h=this.calendars.length;do{b=j(a).date(1);b.day(b.day()++p);a.add("months",1)}while(++lb?(this.disableNextMonth=!1,f.removeClassName(this.container,c.disableNextMonth)):(this.disableNextMonth=!0,f.addClassName(this.container,c.disableNextMonth));else if("today-future"===o.direction||"future"===o.direction)b>=o.months?(this.disablePreviousMonth=!1,f.removeClassName(this.container,
24 | c.disablePreviousMonth)):(this.disablePreviousMonth=!0,f.addClassName(this.container,c.disablePreviousMonth));if("today-past"===o.direction||"past"===o.direction)0>=a.add({y:1}).diff(j(),"months")?(this.disableNextYear=!1,f.removeClassName(this.container,c.disableNextYear)):(this.disableNextYear=!0,f.addClassName(this.container,c.disableNextYear));else if("today-future"===o.direction||"future"===o.direction)0<=a.subtract({y:1}).diff(j(),"months")-(o.months-1)?(this.disablePreviousYear=!1,f.removeClassName(this.container,
25 | c.disablePreviousYear)):(this.disablePreviousYear=!0,f.addClassName(this.container,c.disablePreviousYear))}}};var E=function(a,b,c){var e=[];"string"===typeof a?a=a.split(b):f.isArray(a)||(a=[a]);b=a.length;i=0;do a[i]&&e.push(j(a[i],c).hours(12));while(++i element or a valid element id.";var d=this,g=d.classes;opts=d.settings=f.merge(d.defaults,b);opts.attachTo=window.document.body;opts.selected?e=!0:opts.selected=c.value;q.call(d,opts);if(opts.closeButton){var l=f.make("a",{"class":g.closeButton},d.container);f.addEvent(l,"click",function(){c.blur()})}e&&(c.value=d.getSelected());
32 | e=d.container;var h=!1;e.style.display="none";f.addClassName(e,g.positioned);f.addEvent(e,"mousedown",function(){h=true});f.addEvent(window.document,"mousedown",function(){h=false});f.addEvent(c,"focus",function(){d.setSelected(this.value);d.show()});f.addEvent(c,"blur",function(){if(h){h=false;c.focus()}else d.hide()});f.addEvent(c,"keyup",function(){d.setSelected(this.value)});(g=f.scrollContainer(c))&&f.addEvent(g,"scroll",function(){c.blur()});d.subscribe("change",function(){c.value=d.getSelected()})}};
33 | q.Input.prototype=f.merge(q.prototype,{defaults:f.merge(q.prototype.defaults,{format:"MM/DD/YYYY",side:"bottom",closeButton:!0,offsetLeft:0,offsetTop:0}),classes:f.merge(q.prototype.classes,{positioned:"k-floating",closeButton:"k-btn-close"}),show:function(){var a=this.container,b=a.style,c=this.input,e=f.getPosition(c),d=f.scrollContainer(c),d=d?d.scrollTop:0;b.display="";switch(opts.side){case "left":b.left=e.left-f.getWidth(a)+this.settings.offsetLeft+"px";b.top=e.top+this.settings.offsetTop-d+
34 | "px";break;case "right":b.left=e.left+f.getWidth(c)+"px";b.top=e.top+this.settings.offsetTop-d+"px";break;case "top":b.left=e.left+this.settings.offsetLeft+"px";b.top=e.top-f.getHeight(a)+this.settings.offsetTop-d+"px";break;default:b.left=e.left+this.settings.offsetLeft+"px",b.top=e.top+f.getHeight(c)+this.settings.offsetTop-d+"px"}b.position=f.isFixed(c)?"fixed":"absolute";this.publish("show",this)},hide:function(){this.container.style.display="none";this.publish("hide",this)}});var K=function(a){a||
35 | (a=this);var b=a.c_||{};a.publish=function(a,e,d){for(var g=(a=b[a])?a.length:0,f;g--;)if(f=a[g].apply(e,d||[]),"boolean"===typeof f)return f};a.subscribe=function(a,e,d){b[a]||(b[a]=[]);d?b[a].push(e):b[a].unshift(e);return[a,e]};a.unsubscribe=function(a){for(var e=b[a[0]],a=a[1],d=e?e.length:0;d--;)e[d]===a&&e.splice(d,1)}},p=Date,u=function(a,b){this._d=a;this._isUTC=!!b},r=function(a,b){for(var c=a+"";c.lengtho?"-":"+")+r(~~(Math.abs(o)/60),2)+":"+r(~~(Math.abs(o)%60),2);case "ZZ":return(0>o?"-":"+")+r(~~(10*Math.abs(o)/6),4);case "L":case "LL":case "LLL":case "LLLL":case "LT":return z(a,h.longDateFormat[b]);default:return b.replace(/(^\[)|(\\)|\]$/g,"")}}var e=a.month(),d=a.date(),g=a.year(),f=a.day(),j=a.hours(),q=a.minutes(),n=a.seconds(),o=-a.zone(),m=h.ordinal,k=h.meridiem;return b.replace(N,c)},B=function(a,b){var c=[0,0,1,0,0,0,0],e=0,d=0,g=!1,f=a.match(A),
40 | j=b.match(O),q=Math.min(f.length,j.length),n,o;for(n=0;nk;k++)if(h.monthsParse[k].test(m)){c[1]=k;break}break;case "D":case "DD":case "DDD":case "DDDD":c[2]=~~m;break;case "YY":m=~~m;c[0]=m+(70c[3]&&(c[3]+=12);!1===o&&12===c[3]&&(c[3]=0);c[3]+=e;c[4]+=d;return g?new p(p.UTC.apply({},c)):G(c)},Q=function(a,b,c){var e=h.relativeTime[a];return"function"===typeof e?e(b||1,!!c,a):e.replace(/%d/i,b||1)},H=function(a,b){h.fn[a]=function(a){var e=this._isUTC?"UTC":"";return"undefined"!==typeof a?(this._d["set"+e+b](a),this):this._d["get"+e+b]()}},h,s=Math.round,C={},R=
42 | "undefined"!==typeof module,I="months monthsShort monthsParse weekdays weekdaysShort longDateFormat calendar relativeTime ordinal meridiem".split(" "),v,S=/^\/?Date\((\-?\d+)/i,N=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|zz?|ZZ?|LT|LL?L?L?)/g,M=/[^A-Z]/g,L=/\([A-Za-z ]+\)|:[0-9]{2} [A-Z]{3} /g,O=/(\\)?(MM?M?M?|dd?d?d|DD?D?D?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|ZZ?|T)/g,A=/(\\)?([0-9]+|([a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|([\+\-]\d\d:?\d\d))/gi,
43 | T=/\d{4}.\d\d.\d\d(T(\d\d(.\d\d(.\d\d)?)?)?([\+\-]\d\d:?\d\d)?)?/,J=[["HH:mm:ss",/T\d\d:\d\d:\d\d/],["HH:mm",/T\d\d:\d\d/],["HH",/T\d\d/]],P=/([\+\-]|\d\d)/gi,D="Month Date Hours Minutes Seconds Milliseconds".split(" ");h=function(a,b){if(null===a||""===a)return null;var c;if(a&&a._d instanceof p)c=new p(+a._d);else if(b)if(y(b)){var e=a.match(A),d=99,g,f,h;for(g=0;ge;e++)if(J[e][1].exec(a)){c+=J[e][0];break}c=B(a,c+"Z")}else c=new p(a);else c=new p(a);return new u(c)};h.utc=function(a,b){return y(a)?new u(new p(p.UTC.apply({},a)),!0):b&&a?h(a+" 0",b+" Z").utc():h(a).utc()};h.humanizeDuration=function(a,b,c){var a=
45 | +a,e=h.relativeTime;switch(b){case "seconds":a*=1E3;break;case "minutes":a*=6E4;break;case "hours":a*=36E5;break;case "days":a*=864E5;break;case "weeks":a*=6048E5;break;case "months":a*=2592E6;break;case "years":a*=31536E6;break;default:c=!!b}var b=!c,d=s(Math.abs(a)/1E3),g=s(d/60),f=s(g/60),j=s(f/24),p=s(j/365),d=45>d&&["s",d]||1===g&&["m"]||45>g&&["mm",g]||1===f&&["h"]||22>f&&["hh",f]||1===j&&["d"]||25>=j&&["dd",j]||45>=j&&["M"]||345>j&&["MM",s(j/30)]||1===p&&["y"]||["yy",p];d[2]=b;b=Q.apply({},
46 | d);return c?(0>=a?e.past:e.future).replace(/%s/i,b):b};h.version="1.5.0";h.defaultFormat="YYYY-MM-DDTHH:mm:ssZ";h.lang=function(a,b){var c,e;e=[];if(b){for(c=0;12>c;c++)e[c]=RegExp("^"+b.months[c]+"|^"+b.monthsShort[c].replace(".",""),"i");b.monthsParse=b.monthsParse||e;C[a]=b}if(C[a])for(c=0;ca?c:-1>a?b.lastWeek:0>a?b.lastDay:1>a?b.sameDay:2>a?b.nextDay:7>a?b.nextWeek:c;return this.format("function"===typeof a?a.apply(this):a)},isLeapYear:function(){var a=this.year();return 0===a%4&&0!==a%100||0===a%400},isDST:function(){return this.zone().';
42 | middleware.flash(req, res, function () {});
43 | res.render('error');
44 | }
45 |
46 | app.get('/', middleware.requireRole, middleware.generateMenu(config),
47 | function (req, res) {
48 | if (!config.pages.length || !res.locals.menus[0]) {
49 | noPageError(req, res);
50 | } else {
51 | res.redirect('/pages/' + res.locals.menus[0].id);
52 | }
53 | });
54 |
55 | app.get('/pages/:pageID', middleware.requireRole,
56 | middleware.generateMenu(config), function (req, res) {
57 | var found = config.pages.some(function (page) {
58 | if (page.id === req.params.pageID) {
59 | res.locals.title = page.title + ' - ' + app.locals.settings.title;
60 | res.locals.page = page;
61 | res.render('page');
62 | return true;
63 | }
64 | });
65 | if (!found) {
66 | noPageError(req, res);
67 | }
68 | });
69 |
70 | app.get('/login', function (req, res) {
71 | res.locals.title = 'Login - ' + app.locals.settings.title;
72 | res.render('login');
73 | });
74 |
75 | app.get('/logout', function (req, res) {
76 | req.session.destroy();
77 | res.redirect('/login');
78 | });
79 |
80 | app.post('/login', function (req, res) {
81 | var currentUser;
82 | config.users.forEach(function (user) {
83 | if (user.username === req.body.username &&
84 | user.password === req.body.password) {
85 | currentUser = user;
86 | }
87 | });
88 | if (currentUser) {
89 | req.session.username = currentUser.username;
90 | req.session.role = currentUser.role;
91 | res.redirect(req.query.next || '/');
92 | } else {
93 | req.session.error = 'Wrong username or password.';
94 | if (req.query.next) {
95 | res.redirect('/login?next=' + (req.query.next));
96 | } else {
97 | res.redirect('/login');
98 | }
99 | }
100 | });
101 |
102 | /*
103 | * API
104 | */
105 | app.post('/api/amount_get/:buckets', middleware.requireRole,
106 | function (req, res) {
107 | var buckets = req.params.buckets.split(',');
108 | var len = buckets.length;
109 | var results = [];
110 | buckets.forEach(function (bucket) {
111 | bucket = new ranaly.Amount(bucket);
112 | bucket.get(req.body.time, function (err, result) {
113 | results.push(result);
114 | if (!--len) {
115 | res.json({items: results});
116 | }
117 | });
118 | });
119 | });
120 |
121 | app.post('/api/amount_sum/:buckets', middleware.requireRole, function (req, res) {
122 | var buckets = req.params.buckets.split(',');
123 | var len = buckets.length;
124 | var results = [];
125 | buckets.forEach(function (bucket) {
126 | bucket = new ranaly.Amount(bucket);
127 | bucket.sum(req.body.time, function (err, result) {
128 | results.push(result);
129 | if (!--len) {
130 | res.json({items: results});
131 | }
132 | });
133 | });
134 | });
135 |
136 | app.get('/api/datalist_get/:bucket', middleware.requireRole, function (req, res) {
137 | var bucket = new ranaly.DataList(req.params.bucket);
138 | bucket.len(function (err, length) {
139 | bucket.range(parseInt(req.query.from, 10), parseInt(req.query.to, 10), function (err, list) {
140 | res.json({length: length, data: list});
141 | });
142 | });
143 | });
144 |
145 | var server = http.createServer(app);
146 | server.listen(app.get('port'), function(){
147 | console.log('Ranaly server listening on port ' + app.get('port'));
148 | });
149 |
150 | /*
151 | * Socket.io Server
152 | */
153 | var io = require('socket.io').listen(server, {log: false});
154 | io.enable('browser client etag');
155 | io.set('log level', 1);
156 |
157 | var sessionSockets = new (require('session.socket.io'))(io, sessionStore, cookieParser);
158 |
159 | sub.on('message', function (channel, message) {
160 | io.sockets['in'](channel).emit('realtime value', {
161 | bucketKey: channel,
162 | value: parseInt(message, 10)
163 | });
164 | });
165 |
166 | sessionSockets.on('connection', function (err, socket, session) {
167 | if (!session || !session.username) {
168 | socket.disconnect('unauthorized');
169 | return;
170 | }
171 | socket.on('register realtime bucket', function (bucket, fn) {
172 | bucket = bucket.map(function (b) {
173 | var rl = new ranaly.Realtime(b);
174 | sub.subscribe(rl.channel);
175 | socket.join(rl.channel);
176 | return rl;
177 | });
178 |
179 | var l = bucket.length;
180 | var result = [];
181 | bucket.forEach(function (b, i) {
182 | b.get(function (err, value) {
183 | result[i] = [b.key, value];
184 | if (!--l) {
185 | fn(result);
186 | }
187 | });
188 | });
189 | });
190 | });
191 |
--------------------------------------------------------------------------------
/template/default.mustache:
--------------------------------------------------------------------------------
1 |
2 | {{#data}}
3 | - {{.}}
4 | {{/data}}
5 |
6 |
--------------------------------------------------------------------------------
/template/images.mustache:
--------------------------------------------------------------------------------
1 |
2 | {{#data}}
3 | -
4 |
5 |
6 |
7 |
8 | {{/data}}
9 |
10 |
--------------------------------------------------------------------------------
/views/error.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 | block content
3 | .error
4 | if message
5 | !{message}
6 |
--------------------------------------------------------------------------------
/views/index.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | h1= settings.title
5 | p Welcome to #{settings.title}
6 |
--------------------------------------------------------------------------------
/views/layout.jade:
--------------------------------------------------------------------------------
1 | !!!
2 | html
3 | head
4 | title= title
5 | meta(http-equiv="X-UA-Compatible",content="IE=10")
6 | link(rel='stylesheet', href='/stylesheets/bootstrap.min.css')
7 | link(rel='stylesheet', href='/stylesheets/style.css')
8 | link(rel='stylesheet', href='/kalendae/kalendae.css')
9 | script(type="text/javascript", src="/javascripts/lib/jquery.js")
10 | body
11 | .wrap
12 | block content
13 | script(type="text/javascript", src="/javascripts/lib/highcharts.js")
14 | script(type="text/javascript", src="/javascripts/lib/highcharts_exporting.js")
15 | script(type="text/javascript", src="/kalendae/kalendae.min.js")
16 | script(type="text/javascript", src="/javascripts/lib/underscore.js")
17 | // script(type="text/javascript", src="/javascripts/async.js")
18 | script(type="text/javascript", src="/javascripts/lib/bootstrap.js")
19 | script(type="text/javascript", src="/javascripts/lib/mustache.js")
20 | script(type="text/javascript", src="/javascripts/ranaly.js")
21 | script(type="text/javascript", src="/socket.io/socket.io.js")
22 |
--------------------------------------------------------------------------------
/views/login.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 | block content
3 |
4 | .login
5 | .col
6 | h2 Login
7 | if message
8 | !{message}
9 | form.form-horizontal(method="post")
10 | div.control-group
11 | label.control-label(for='username') Username
12 | div.controls
13 | input#username(name="username", type='text',placeholder='Username')
14 | div.control-group
15 | label.control-label(for='password') Password
16 | div.controls
17 | input#password(name="password", type='password',placeholder='Password')
18 | div.control-group
19 | div.controls
20 | button.btn(type='submit') Sign in
21 | .footer
22 | p Powered by
23 | a(href="https://github.com/luin/ranaly") Ranaly
24 | .
25 |
--------------------------------------------------------------------------------
/views/page.jade:
--------------------------------------------------------------------------------
1 | extends page_layout
2 | include widgets/amount_line_chart
3 | include widgets/amount_pie_chart
4 | include widgets/amount_today_count
5 | include widgets/amount_total_count
6 | include widgets/realtime_line_chart
7 | include widgets/realtime_count
8 | include widgets/datalist_list
9 | include widgets/custom_code
10 |
11 | block page_content
12 | for widget in page.widgets
13 | if widget.role <= user.role
14 | if widget.type == "amount_line_chart"
15 | +amount_line_chart(widget)
16 | else if widget.type == "amount_pie_chart"
17 | +amount_pie_chart(widget)
18 | else if widget.type == "amount_today_count"
19 | +amount_today_count(widget)
20 | else if widget.type == "amount_total_count"
21 | +amount_total_count(widget)
22 | else if widget.type == "realtime_line_chart"
23 | +realtime_line_chart(widget)
24 | else if widget.type == "realtime_count"
25 | +realtime_count(widget)
26 | else if widget.type == "datalist_list"
27 | +datalist_list(widget)
28 | else if widget.type == "custom_code"
29 | +custom_code(widget)
30 | else
31 | p No widgets.
32 | .footer
33 | p Powered by
34 | a(href="https://github.com/luin/ranaly") Ranaly
35 | .
36 |
--------------------------------------------------------------------------------
/views/page_layout.jade:
--------------------------------------------------------------------------------
1 | extends layout
2 |
3 | block content
4 | .navbar
5 | .navbar-inner
6 | a.brand(href="/") #{config.app_name}
7 | ul.nav
8 | for _page in menus
9 | if _page.id == page.id
10 | li.active
11 | a(href="/pages/" + _page.id) #{_page.title}
12 | else
13 | li
14 | a(href="/pages/" + _page.id) #{_page.title}
15 | ul.nav.pull-right
16 | li.dropdown
17 | a.dropdown-toggle(href='#',data-toggle='dropdown')
18 | | #{user.username}
19 | b.caret
20 | ul.dropdown-menu
21 | li
22 | a(href="https://github.com/luin/ranaly", target="_blank") Help
23 | li.divider
24 | li
25 | a(href="/logout") Logout
26 | block page_content
27 |
--------------------------------------------------------------------------------
/views/widgets/amount_line_chart.jade:
--------------------------------------------------------------------------------
1 | mixin amount_line_chart(widget)
2 | .col
3 | h2 #{widget.title}
4 | .pull-right
5 | .btn-group
6 | button.btn.btn-small(id='bt' + widget.id)
7 | button.btn.btn-small.dropdown-toggle(data-toggle='dropdown')
8 | span.caret
9 | ul.dropdown-menu(id='dropdown' + widget.id)
10 | li(data-range='today')
11 | a(href='javascript:void 0') Today
12 | li(data-range='yesterday')
13 | a(href='javascript:void 0') Yesterday
14 | li.divider
15 | li(data-range='7days')
16 | a(href='javascript:void 0') Last 7 days
17 | li(data-range='30days')
18 | a(href='javascript:void 0') Last 30 days
19 | li.divider
20 | li(data-range='custom', data-toggle='modal', data-target='#modal' + widget.id)
21 | a(href='javascript:void 0') Custom...
22 | div(id='container' + widget.id)
23 | .modal.hide.fade(id='modal' + widget.id)
24 | .modal-header
25 | button.close(type='button',data-dismiss='modal',aria-hidden='true') ×
26 | h3 Custom date range
27 | .modal-body
28 | .div(id='date' + widget.id)
29 | .modal-footer
30 | button.btn(data-dismiss='modal',aria-hidden='true') Cancel
31 | button.btn.btn-primary(id='okBtn' + widget.id) OK
32 | script
33 | $(function () {
34 | var widget = !{JSON.stringify(widget)};
35 | var id = widget.id;
36 | var silent = false;
37 | var option = {
38 | chart: {
39 | renderTo: 'container' + id
40 | },
41 | title: {
42 | text: widget.title
43 | },
44 | subtitle: {
45 | text: widget.subtitle
46 | },
47 | xAxis: {
48 | type: 'datetime'
49 | },
50 | yAxis: {
51 | title: {
52 | text: widget.y_axis_title
53 | },
54 | min: 0
55 | },
56 | series: (function () {
57 | var result = [];
58 | widget.bucket.forEach(function (bucket) {
59 | result.push({
60 | name: bucket
61 | });
62 | });
63 | return result;
64 | })()
65 | };
66 |
67 | var chart = new Highcharts.Chart(option);
68 | var kal = new Kalendae('date' + id, {
69 | mode: 'range',
70 | months: 2
71 | });
72 |
73 | var loadData = function (from, to) {
74 | if (!silent)
75 | chart.showLoading();
76 | // Update the kal
77 | var mfrom = moment(from).format('M/D/YYYY');
78 | if (to) {
79 | var mto = moment(to).format('M/D/YYYY');
80 | kal.setSelected(mfrom + ' - ' + mto);
81 | } else {
82 | kal.setSelected(mfrom);
83 | }
84 |
85 | if (!to) {
86 | from = startOfDay(from);
87 | }
88 | var timeList = [];
89 | if (!to) {
90 | for (var i = 0; i < 24; ++i) {
91 | timeList.push(new Date(from.getTime() + i * 3600 * 1000));
92 | }
93 | } else {
94 | to = to.getTime();
95 | while(from.getTime() <= to) {
96 | timeList.push(from);
97 | from = new Date(from.getTime() + 24 * 3600 * 1000);
98 | }
99 | }
100 |
101 | var timeToString = function (time) {
102 | if (to)
103 | return moment(time).format('YYYYMMDD');
104 | else
105 | return moment(time).format('YYYYMMDDHH');
106 | }
107 | ranalyApi.amount.get(widget.bucket, _.map(timeList, timeToString), function (data) {
108 | data = data.items;
109 | for (var i = 0; i < data.length; ++i) {
110 | for (var j = 0; j < data[i].length; ++j) {
111 | data[i][j] = [timeList[j].getTime(), data[i][j]];
112 | }
113 | chart.series[i].setData(data[i], false);
114 | }
115 | chart.redraw();
116 | chart.hideLoading();
117 | silent = false;
118 | });
119 | };
120 |
121 | $('#dropdown' + id).delegate('li', 'click', function () {
122 | var range = $(this).attr('data-range');
123 | if (range === 'custom') return;
124 | var now = new Date();
125 | var yesterday = new Date(now.getTime() - 24 * 3600 * 1000);
126 | $('#bt' + id).attr('data-range', range);
127 | $('#bt' + id).html($(this).find('a').html());
128 | switch(range) {
129 | case 'today':
130 | loadData(startOfDay(now));
131 | break;
132 | case 'yesterday':
133 | loadData(startOfDay(yesterday));
134 | break;
135 | case '30days':
136 | loadData(new Date(Date.now() - 30 * 24 * 3600 * 1000), new Date());
137 | break;
138 | case '7days':
139 | loadData(new Date(Date.now() - 7 * 24 * 3600 * 1000), new Date());
140 | break;
141 | }
142 | });
143 |
144 | $('#bt' + id).click(function () {
145 | var dataRange = $(this).attr('data-range');
146 | if (dataRange === 'custom') {
147 | $('#okBtn' + id).click();
148 | } else {
149 | $('#dropdown' + id + ' [data-range='+dataRange+']').click();
150 | }
151 | });
152 |
153 | $('#okBtn' + id).click(function () {
154 | $('#modal' + id).modal('hide');
155 | var res = kal.getSelectedAsDates();
156 | if (!res[1] || res[0].getTime() === res[1].getTime()) {
157 | loadData(res[0]);
158 | } else {
159 | loadData(res[0], res[1]);
160 | }
161 | $('#bt' + id).attr('data-range', 'custom');
162 | $('#bt' + id).html(kal.getSelected());
163 | });
164 |
165 | $('#dropdown' + id + ' [data-range=' + widget.default_range + ']').click();
166 |
167 | renderInterval(widget.update_interval, function () {
168 | silent = true;
169 | $('#bt' + id).click();
170 | });
171 | });
172 |
--------------------------------------------------------------------------------
/views/widgets/amount_pie_chart.jade:
--------------------------------------------------------------------------------
1 | mixin amount_pie_chart(widget)
2 | .col
3 | .amount-pie-chart-total(id="total" + widget.id)
4 | h2 #{widget.title}
5 | .pull-right
6 | .btn-group
7 | button.btn.btn-small(id='bt' + widget.id)
8 | button.btn.btn-small.dropdown-toggle(data-toggle='dropdown')
9 | span.caret
10 | ul.dropdown-menu(id='dropdown' + widget.id)
11 | li(data-range='today')
12 | a(href='javascript:void 0') Today
13 | li(data-range='yesterday')
14 | a(href='javascript:void 0') Yesterday
15 | li.divider
16 | li(data-range='7days')
17 | a(href='javascript:void 0') Last 7 days
18 | li(data-range='30days')
19 | a(href='javascript:void 0') Last 30 days
20 | li.divider
21 | li(data-range='all')
22 | a(href='javascript:void 0') All
23 | li.divider
24 | li(data-range='custom')
25 | a(href='javascript:void 0', data-toggle='modal', data-target='#modal' + widget.id) Custom...
26 | div(id='container' + widget.id)
27 | .modal.hide.fade(id='modal' + widget.id)
28 | .modal-header
29 | button.close(type='button',data-dismiss='modal',aria-hidden='true') ×
30 | h3 Custom date range
31 | .modal-body
32 | .div(id='date' + widget.id)
33 | .modal-footer
34 | button.btn(data-dismiss='modal',aria-hidden='true') Cancel
35 | button.btn.btn-primary(id='okBtn' + widget.id) OK
36 | script
37 | $(function () {
38 | var widget = !{JSON.stringify(widget)};
39 | var id = widget.id;
40 | var silent = false;
41 | var option = {
42 | chart: {
43 | renderTo: 'container' + id
44 | },
45 | title: {
46 | text: '#{widget.title}'
47 | },
48 | subtitle: {
49 | text: '#{widget.subtitle}'
50 | },
51 | tooltip: {
52 | formatter: function() {
53 | return ''+ this.point.name +': '+ this.percentage.toFixed(2) +' %';
54 | }
55 | },
56 | plotOptions: {
57 | pie: {
58 | allowPointSelect: true,
59 | cursor: 'pointer',
60 | dataLabels: {
61 | enabled: true,
62 | color: '#000000',
63 | connectorColor: '#000000',
64 | formatter: function() {
65 | return ''+ this.point.name +': '+ this.y;
66 | }
67 | }
68 | }
69 | },
70 | series: [{
71 | type: 'pie',
72 | data: []
73 | }]
74 | };
75 |
76 | var chart = new Highcharts.Chart(option);
77 | var kal = new Kalendae('date' + id, {
78 | mode: 'range',
79 | months: 2
80 | });
81 |
82 | var loadData = function (from, to) {
83 | var timeList = [];
84 | if (!silent)
85 | chart.showLoading();
86 | if (from) {
87 | // Update the kal
88 | var mfrom = moment(from).format('M/D/YYYY');
89 | if (to) {
90 | var mto = moment(to).format('M/D/YYYY');
91 | kal.setSelected(mfrom + ' - ' + mto);
92 | } else {
93 | kal.setSelected(mfrom);
94 | }
95 |
96 | if (!to) {
97 | from = startOfDay(from);
98 | }
99 | if (!to) {
100 | for (var i = 0; i < 24; ++i) {
101 | timeList.push(new Date(from.getTime() + i * 3600 * 1000));
102 | }
103 | } else {
104 | to = to.getTime();
105 | while(from.getTime() <= to) {
106 | timeList.push(from);
107 | from = new Date(from.getTime() + 24 * 3600 * 1000);
108 | }
109 | }
110 | }
111 | var timeToString = function (time) {
112 | if (to)
113 | return moment(time).format('YYYYMMDD');
114 | else
115 | return moment(time).format('YYYYMMDDHH');
116 | }
117 | ranalyApi.amount.sum(widget.bucket, _.map(timeList, timeToString), function (data) {
118 | data = data.items;
119 | var i = 0;
120 | var total = 0;
121 | data = data.map(function (d) {
122 | total += d;
123 | return [widget.bucket[i++], d]
124 | });
125 | if (!total) {
126 | data = [['No data', 1]];
127 | }
128 | $('#total' + id).html('Total: ' + total);
129 | chart.series[0].setData(data);
130 | chart.hideLoading();
131 | silent = false;
132 | });
133 | };
134 |
135 | $('#dropdown' + id).delegate('li', 'click', function () {
136 | var range = $(this).attr('data-range');
137 | if (range === 'custom') return;
138 | var now = new Date();
139 | var yesterday = new Date(now.getTime() - 24 * 3600 * 1000);
140 | $('#bt' + id).attr('data-range', range);
141 | $('#bt' + id).html($(this).find('a').html());
142 | switch(range) {
143 | case 'today':
144 | loadData(startOfDay(now));
145 | break;
146 | case 'yesterday':
147 | loadData(startOfDay(yesterday));
148 | break;
149 | case '30days':
150 | loadData(new Date(Date.now() - 30 * 24 * 3600 * 1000), new Date());
151 | break;
152 | case '7days':
153 | loadData(new Date(Date.now() - 7 * 24 * 3600 * 1000), new Date());
154 | break;
155 | case 'all':
156 | loadData();
157 | break;
158 | }
159 | });
160 |
161 | $('#bt' + id).click(function () {
162 | var dataRange = $(this).attr('data-range');
163 | if (dataRange === 'custom') {
164 | $('#okBtn' + id).click();
165 | } else {
166 | $('#dropdown' + id + ' [data-range='+dataRange+']').click();
167 | }
168 | });
169 |
170 | $('#okBtn' + id).click(function () {
171 | $('#modal' + id).modal('hide');
172 | var res = kal.getSelectedAsDates();
173 | if (!res[1] || res[0].getTime() === res[1].getTime()) {
174 | loadData(res[0]);
175 | } else {
176 | loadData(res[0], res[1]);
177 | }
178 | $('#bt' + id).attr('data-range', 'custom');
179 | $('#bt' + id).html(kal.getSelected());
180 | });
181 |
182 | $('#dropdown' + id + ' [data-range=' + widget.default_range + ']').click();
183 |
184 | renderInterval(widget.update_interval, function () {
185 | silent = true;
186 | $('#bt' + id).click();
187 | });
188 | });
189 |
--------------------------------------------------------------------------------
/views/widgets/amount_today_count.jade:
--------------------------------------------------------------------------------
1 | mixin amount_today_count(widget)
2 | style
3 | div.clearfix(id='container' + widget.id)
4 | script
5 | $(function () {
6 | var widget = !{JSON.stringify(widget)};
7 | var id = widget.id;
8 |
9 | var tpl = ' \
10 | {{#data}} \
11 | \
12 | \
13 | \
14 | TODAY\'S {{bucket}} \
15 | | \
16 | \
17 | {{today}} \
18 | | \
19 |
\
20 | \
21 | \
22 | ESTIMATE \
23 | | \
24 | \
25 | {{estimate}} \
26 | | \
27 |
\
28 | \
29 | \
30 | VS yesterday \
31 | | \
32 | \
33 | {{trend}}% \
34 | | \
35 |
\
36 |
\
37 | {{/data}} \
38 | ';
39 | var execute = function () {
40 | var today = moment();
41 | var todayHour = today.hours();
42 | var todayYYYYMMDD = today.format('YYYYMMDD');
43 | var yesterday = moment(Date.now() - 24 * 3600 * 1000);
44 | var yesterdayYYYYMMDD = yesterday.format('YYYYMMDD');
45 | var todayHours = [];
46 | var yesterdayHours = [];
47 | for (var i = 0; i < todayHour; ++i) {
48 | var d = (i < 10) ? '0' + i : i.toString();
49 | todayHours.push(todayYYYYMMDD + d);
50 | }
51 | for (var i = 0; i < 24; ++i) {
52 | var d = (i < 10) ? '0' + i : i.toString();
53 | yesterdayHours.push(yesterdayYYYYMMDD + d);
54 | }
55 |
56 | ranalyApi.amount.get(widget.bucket, [todayYYYYMMDD].concat(todayHours).concat(yesterdayHours), function (data) {
57 | data = data.items;
58 | var locals = {data: []};
59 | var i = 0, j = 0;
60 | _.forEach(data, function (d) {
61 | var iLocals = {};
62 | iLocals.color = CHART_COLORS[i % CHART_COLORS.length];
63 | iLocals.bucket = widget.bucket[i++];
64 | iLocals.today = d[0];
65 | var todayTrend = 0;
66 | for (j = 0; j < todayHour; ++j) {
67 | todayTrend += d[1 + j];
68 | }
69 | var yesterdayTrend = 0;
70 | for (j = 0; j < todayHour; ++j) {
71 | yesterdayTrend += d[1 + todayHour + j];
72 | }
73 | if (yesterdayTrend) {
74 | iLocals.trend = ((todayTrend - yesterdayTrend) * 100 / yesterdayTrend).toFixed(2);
75 | } else {
76 | iLocals.trend = 0;
77 | }
78 | iLocals.class = (iLocals.trend >= 0) ? 'trend-up' : 'trend-down';
79 | var yesterdayTotal = yesterdayTrend;
80 | for (j = todayHour; j < 24; ++j) {
81 | yesterdayTotal += d[1 + todayHour + j];
82 | }
83 | if (yesterdayTrend) {
84 | iLocals.estimate = parseInt(todayTrend / yesterdayTrend * yesterdayTotal, 10);
85 | } else {
86 | iLocals.estimate = yesterdayTotal;
87 | }
88 | locals.data.push(iLocals);
89 | });
90 | $('#container' + id).html(Mustache.render(tpl, locals));
91 | });
92 | };
93 | execute();
94 | renderInterval(widget.update_interval, function () {
95 | execute();
96 | });
97 | });
98 |
--------------------------------------------------------------------------------
/views/widgets/amount_total_count.jade:
--------------------------------------------------------------------------------
1 | mixin amount_total_count(widget)
2 | .col
3 | h2 #{widget.title}
4 | .pull-right
5 | .btn-group
6 | button.btn.btn-small(id='bt' + widget.id)
7 | button.btn.btn-small.dropdown-toggle(data-toggle='dropdown')
8 | span.caret
9 | ul.dropdown-menu(id='dropdown' + widget.id)
10 | li(data-range='today')
11 | a(href='javascript:void 0') Today
12 | li(data-range='yesterday')
13 | a(href='javascript:void 0') Yesterday
14 | li.divider
15 | li(data-range='7days')
16 | a(href='javascript:void 0') Last 7 days
17 | li(data-range='30days')
18 | a(href='javascript:void 0') Last 30 days
19 | li.divider
20 | li(data-range='all')
21 | a(href='javascript:void 0') All
22 | li.divider
23 | li(data-range='custom')
24 | a(href='javascript:void 0', data-toggle='modal', data-target='#modal' + widget.id) Custom...
25 | div(id='container' + widget.id)
26 | .modal.hide.fade(id='modal' + widget.id)
27 | .modal-header
28 | button.close(type='button',data-dismiss='modal',aria-hidden='true') ×
29 | h3 Custom date range
30 | .modal-body
31 | .div(id='date' + widget.id)
32 | .modal-footer
33 | button.btn(data-dismiss='modal',aria-hidden='true') Cancel
34 | button.btn.btn-primary(id='okBtn' + widget.id) OK
35 | script
36 | $(function () {
37 | var widget = !{JSON.stringify(widget)};
38 | var id = widget.id;
39 | var kal = new Kalendae('date' + id, {
40 | mode: 'range',
41 | months: 2
42 | });
43 | var tpl = ' \
44 | \
45 | {{#data}} \
46 | - \
47 |
{{count}}
\
48 | {{bucket}}
\
49 | \
50 | {{/data}} \
51 |
\
52 | ';
53 |
54 | var loadData = function (from, to) {
55 | var timeList = [];
56 | if (from) {
57 | // Update the kal
58 | var mfrom = moment(from).format('M/D/YYYY');
59 | if (to) {
60 | var mto = moment(to).format('M/D/YYYY');
61 | kal.setSelected(mfrom + ' - ' + mto);
62 | } else {
63 | kal.setSelected(mfrom);
64 | }
65 |
66 | if (!to) {
67 | from = startOfDay(from);
68 | }
69 | if (!to) {
70 | for (var i = 0; i < 24; ++i) {
71 | timeList.push(new Date(from.getTime() + i * 3600 * 1000));
72 | }
73 | } else {
74 | to = to.getTime();
75 | while(from.getTime() <= to) {
76 | timeList.push(from);
77 | from = new Date(from.getTime() + 24 * 3600 * 1000);
78 | }
79 | }
80 | }
81 | var timeToString = function (time) {
82 | if (to)
83 | return moment(time).format('YYYYMMDD');
84 | else
85 | return moment(time).format('YYYYMMDDHH');
86 | }
87 | ranalyApi.amount.sum(widget.bucket, _.map(timeList, timeToString), function (data) {
88 | data = data.items;
89 | var locals = {data: []};
90 | var i = 0;
91 | data = data.map(function (d) {
92 | locals.data.push({
93 | count: d,
94 | color: CHART_COLORS[i % CHART_COLORS.length],
95 | bucket: widget.bucket[i++]
96 | });
97 | });
98 | $('#container' + id).html(Mustache.render(tpl, locals));
99 | });
100 | };
101 |
102 | $('#dropdown' + id).delegate('li', 'click', function () {
103 | var range = $(this).attr('data-range');
104 | if (range === 'custom') return;
105 | var now = new Date();
106 | var yesterday = new Date(now.getTime() - 24 * 3600 * 1000);
107 | $('#bt' + id).attr('data-range', range);
108 | $('#bt' + id).html($(this).find('a').html());
109 | switch(range) {
110 | case 'today':
111 | loadData(startOfDay(now));
112 | break;
113 | case 'yesterday':
114 | loadData(startOfDay(yesterday));
115 | break;
116 | case '30days':
117 | loadData(new Date(Date.now() - 30 * 24 * 3600 * 1000), new Date());
118 | break;
119 | case '7days':
120 | loadData(new Date(Date.now() - 7 * 24 * 3600 * 1000), new Date());
121 | break;
122 | case 'all':
123 | loadData();
124 | break;
125 | }
126 | });
127 |
128 | $('#bt' + id).click(function () {
129 | var dataRange = $(this).attr('data-range');
130 | if (dataRange === 'custom') {
131 | $('#okBtn' + id).click();
132 | } else {
133 | $('#dropdown' + id + ' [data-range='+dataRange+']').click();
134 | }
135 | });
136 |
137 | $('#okBtn' + id).click(function () {
138 | $('#modal' + id).modal('hide');
139 | var res = kal.getSelectedAsDates();
140 | if (!res[1] || res[0].getTime() === res[1].getTime()) {
141 | loadData(res[0]);
142 | } else {
143 | loadData(res[0], res[1]);
144 | }
145 | $('#bt' + id).attr('data-range', 'custom');
146 | $('#bt' + id).html(kal.getSelected());
147 | });
148 |
149 | $('#dropdown' + id + ' [data-range=' + widget.default_range + ']').click();
150 |
151 | renderInterval(widget.update_interval, function () {
152 | $('#bt' + id).click();
153 | });
154 | });
155 |
--------------------------------------------------------------------------------
/views/widgets/custom_code.jade:
--------------------------------------------------------------------------------
1 | mixin custom_code(widget)
2 | !{widget.content}
3 |
--------------------------------------------------------------------------------
/views/widgets/datalist_list.jade:
--------------------------------------------------------------------------------
1 | mixin datalist_list(widget)
2 | .col
3 | h2 #{widget.title}
4 | .clearfix(id="container" + widget.id)
5 | .content
6 | .pagination(style="margin-bottom: -10px; float: right")
7 | script
8 | var widget = !{JSON.stringify(widget)};
9 | var currentPage#{widget.id} = 1;
10 | var datalist_list_load_data#{widget.id} = function (page) {
11 | currentPage#{widget.id} = page;
12 | var widget = !{JSON.stringify(widget)};
13 | var tpl = ' \
14 | \
15 | {{#li}} \
16 | - \
17 | {{page}} \
18 |
\
19 | {{/li}} \
20 |
\
21 | ';
22 | var from = (page - 1) * #{widget.count_per_page};
23 | var to = from + #{widget.count_per_page} - 1;
24 | ranalyApi.datalist.get('#{widget.bucket[0]}', from, to, function (result) {
25 | var length = Math.ceil(result.length / #{widget.count_per_page});
26 | var locals = {li: []};
27 | for (var i = 1; i <= length; ++i) {
28 | locals.li.push({
29 | class: (i === page) ? ' class=active' : '',
30 | page: i
31 | });
32 | }
33 | $('#container#{widget.id} .pagination').html(Mustache.render(tpl, locals));
34 | $('#container#{widget.id} .content').html(Mustache.render(widget.template, result));
35 | });
36 |
37 | }
38 | $(function () {
39 | datalist_list_load_data#{widget.id}(currentPage#{widget.id});
40 | renderInterval(widget.update_interval, function () {
41 | datalist_list_load_data#{widget.id}(currentPage#{widget.id});
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/views/widgets/realtime_count.jade:
--------------------------------------------------------------------------------
1 | mixin realtime_count(widget)
2 | .col
3 | h2 #{widget.title}
4 | div(id="container" + widget.id)
5 | script
6 | $(function () {
7 | var widget = !{JSON.stringify(widget)}
8 | var socket = io.connect('http://' + document.location.host);
9 | var keyToID = {};
10 | var generateID = function (prefix) {
11 | prefix = prefix || '';
12 | return prefix + (Math.random() * 1000000000 | 0).toString(36);
13 | };
14 | var tpl = ' \
15 | \
16 | {{#data}} \
17 | - \
18 |
{{count}}
\
19 | {{bucket}}
\
20 | \
21 | {{/data}} \
22 |
\
23 | ';
24 | socket.on('connect', function () {
25 | socket.emit('register realtime bucket', widget.bucket, function (value) {
26 | var i = 0;
27 | var locals = {data: []};
28 | value.forEach(function (v) {
29 | var id = generateID('realtimeCount');
30 | keyToID[v[0]] = id;
31 | locals.data.push({
32 | count: v[1],
33 | color: CHART_COLORS[i % CHART_COLORS.length],
34 | bucket: widget.bucket[i],
35 | id: id
36 | });
37 | i += 1;
38 | });
39 | $('#container' + widget.id).html(Mustache.render(tpl, locals));
40 | });
41 | });
42 | socket.on('realtime value', function (data) {
43 | var id = keyToID[data.bucketKey];
44 | if (typeof id === 'string') {
45 | document.getElementById(id).innerHTML = data.value;
46 | }
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/views/widgets/realtime_line_chart.jade:
--------------------------------------------------------------------------------
1 | mixin realtime_line_chart(widget)
2 | .col
3 | h2 #{widget.title}
4 | div(id="container" + widget.id)
5 | script
6 | $(function () {
7 | var widget = !{JSON.stringify(widget)}
8 | var socket = io.connect('http://' + document.location.host);
9 | var bucketValue = {};
10 | var bucketToKey = {};
11 | socket.on('connect', function () {
12 | socket.emit('register realtime bucket', widget.bucket, function (value) {
13 | var i = 0;
14 | value.forEach(function (v) {
15 | bucketValue[v[0]] = v[1];
16 | bucketToKey[widget.bucket[i]] = v[0];
17 | i += 1;
18 | });
19 | });
20 | });
21 | socket.on('realtime value', function (data) {
22 | if (typeof bucketValue[data.bucketKey] === 'number') {
23 | bucketValue[data.bucketKey] = data.value;
24 | }
25 | });
26 |
27 | var chart;
28 | chart = new Highcharts.Chart({
29 | chart: {
30 | renderTo: 'container' + widget.id,
31 | type: 'spline',
32 | events: {
33 | load: function () {
34 | var that = this;
35 | setInterval(function () {
36 | var x = (new Date()).getTime();
37 | var i = 0;
38 | widget.bucket.forEach(function (b) {
39 | var series = that.series[i];
40 | var key = bucketToKey[b];
41 | series.addPoint([x, bucketValue[key]], false, true);
42 | i += 1;
43 | });
44 | chart.redraw();
45 | }, 1000);
46 | }
47 | }
48 | },
49 | title: {
50 | text: widget.title
51 | },
52 | xAxis: {
53 | type: 'datetime',
54 | tickPixelInterval: 150
55 | },
56 | yAxis: {
57 | title: {
58 | text: widget.y_axis_title
59 | },
60 | plotLines: [{
61 | value: 0,
62 | width: 1,
63 | color: '#808080'
64 | }]
65 | },
66 | tooltip: {
67 | formatter: function () {
68 | return '' + this.series.name + '
' + Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) + '
' + Highcharts.numberFormat(this.y, 2);
69 | }
70 | },
71 | legend: {
72 | enabled: true
73 | },
74 | exporting: {
75 | enabled: false
76 | },
77 | series: (function () {
78 | return widget.bucket.map(function (b) {
79 | var res = {name: b, data: []};
80 | var time = (new Date()).getTime();
81 | for (var i = -19; i <= 0; i++) {
82 | res.data.push({
83 | x: time + i * 1000,
84 | y: 0
85 | });
86 | }
87 | return res;
88 | });
89 | })()
90 | });
91 | });
92 |
--------------------------------------------------------------------------------