├── .gitignore ├── LICENSE ├── README.md ├── assets └── Chart.min.js ├── chartjs.go ├── examples ├── reckoning-multiple-instances │ └── main.go └── reckoning-single-instance │ └── main.go ├── random-sets.png ├── reckon.go ├── stats.go ├── stats_test.go ├── templates.go ├── templates_html.go └── templates_text.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | 193 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reckon 2 | 3 | > A Go package for sampling and reporting on random keys on a set of redis instances 4 | 5 | [![GoDoc](https://godoc.org/github.com/zulily/reckon?status.svg)](https://godoc.org/github.com/zulily/reckon) 6 | 7 | Inspired/influenced by [redis-sampler](https://github.com/antirez/redis-sampler) 8 | from [antirez](https://github.com/antirez), the author of redis. 9 | 10 | ## Background 11 | 12 | We love redis here at [zulily](https://github.com/zulily/). We store millions 13 | of keys across many redis instances, and we've built our own internal distributed 14 | cache on top of it. 15 | 16 | One problem with running a large, distributed cache using redis is the opaque 17 | nature of the keyspaces; it's hard to tell what the composition of your redis 18 | dataset is, especially when you've got multiple codebases or teams using the 19 | same redis instance(s), or you're sharding your dataset over a large number of 20 | redis instances. 21 | 22 | While there are some [existing](https://github.com/antirez/redis-sampler) 23 | [solutions](https://github.com/snmaynard/redis-audit) for sampling a redis 24 | keyspace, the `reckon` package has a few advantages: 25 | 26 | ### Programmatic access to sampling results 27 | 28 | Results are returned in data structures, not just printed to stdout or a file. 29 | This is what allows a user of reckon to sample data across a cluster of redis 30 | instances and merge the results to get an overall picture of the keyspaces. 31 | We've included some sample code to do just that, in the 32 | [examples](https://github.com/zulily/reckon/tree/master/examples/reckoning-multiple-instances). 33 | 34 | ### Aggregation 35 | 36 | `reckon` also allows you to define arbitrary buckets based on the name of the 37 | sampled key and/or the redis data type (hash, set, list, etc.). During 38 | sampling, `reckon` compiles statistics about the various redis data types, and 39 | aggregates those statistics according to the buckets you defined. 40 | 41 | Any type that implements the `Aggregator` interface can instruct `reckon` as to 42 | how to aggregate the redis keys that it samples. This is best illustrated with some 43 | simple, contrived examples: 44 | 45 | To aggregate only redis sets whose keys start with the letter a: 46 | 47 | func setsThatStartWithA(key string, valueType reckon.ValueType) []string { 48 | if strings.HasPrefix(key, "a") && valueType == reckon.TypeSet { 49 | return []string{"setsThatStartWithA"} 50 | } 51 | return []string{} 52 | } 53 | 54 | To aggregate sampled keys of any redis data type that are longer than 80 characters: 55 | 56 | func longKeys(key string, valueType reckon.ValueType) []string { 57 | if len(key) > 80 { 58 | return []string{"long-keys"} 59 | } 60 | return []string{} 61 | } 62 | 63 | ### Reports 64 | 65 | When you are done sampling, aggregating, and/or combining the results produced 66 | by `reckon` you can easily produce a report of the findings in either plain-text 67 | or static HTML. An example HTML report is shown below: 68 | 69 | ![Sample HTML report](https://github.com/zulily/reckon/blob/master/random-sets.png) 70 | 71 | 72 | ## Quick Start 73 | 74 | Get the code: 75 | 76 | $ go get github.com/zulily/reckon 77 | 78 | Build the example binaries: 79 | 80 | $ cd $GOPATH/src/github.com/zulily/reckon 81 | $ go install -v ./... 82 | 83 | Use one of the provided example binaries to sample from a redis instance and 84 | output results to static HTML files in the current directory: 85 | 86 | $ reckoning-single-instance -host=localhost -port=6379 \ 87 | -sample-rate=0.1 -min-samples=100 88 | 89 | Or to sample from multiple instances: 90 | 91 | $ reckoning-multiple-instances -sample-rate=0.1 \ 92 | -redis=localhost:6379 \ 93 | -redis=localhost:6380 \ 94 | -redis=localhost:6381 95 | 96 | Or, use the package in your own binary: 97 | 98 | package main 99 | 100 | import ( 101 | "log" 102 | "os" 103 | 104 | "github.com/zulily/reckon" 105 | ) 106 | 107 | func main() { 108 | 109 | opts := reckon.Options{ 110 | Host: "localhost", 111 | Port: 6379, 112 | MinSamples: 10000, 113 | } 114 | 115 | stats, keyCount, err := reckon.Run(opts, reckon.AggregatorFunc(reckon.AnyKey)) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | log.Printf("total key count: %d\n", keyCount) 121 | for k, v := range stats { 122 | log.Printf("stats for: %s\n", k) 123 | 124 | v.Name = k 125 | if f, err := os.Create(fmt.Sprintf("output-%s.html", k)); err != nil { 126 | panic(err) 127 | } else { 128 | defer f.Close() 129 | log.Printf("Rendering totals for: '%s' to %s:\n", k, f.Name()) 130 | if err := reckon.RenderHTML(v, f); err != nil { 131 | panic(err) 132 | } 133 | } 134 | } 135 | } 136 | 137 | ## Limitations 138 | 139 | Since `reckon` makes use of redis' `RANDOMKEY` and `INFO` commands, it is not 140 | able to sample data via a [twemproxy](https://github.com/twitter/twemproxy) 141 | proxy, since twemproxy implements a subset of the redis protocol that does not 142 | include these commands. 143 | 144 | However, instead of sampling through a proxy, you can easily run `reckon` 145 | against multiple redis instances, and merge the results. We include code 146 | that does just that in the 147 | [examples](https://github.com/zulily/reckon/tree/master/examples/reckoning-multiple-instances). 148 | -------------------------------------------------------------------------------- /assets/Chart.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Chart.js 3 | * http://chartjs.org/ 4 | * Version: 1.0.2 5 | * 6 | * Copyright 2015 Nick Downie 7 | * Released under the MIT license 8 | * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md 9 | */ 10 | (function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); -------------------------------------------------------------------------------- /chartjs.go: -------------------------------------------------------------------------------- 1 | package reckon 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | func bindata_read(data []byte, name string) ([]byte, error) { 17 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 18 | if err != nil { 19 | return nil, fmt.Errorf("Read %q: %v", name, err) 20 | } 21 | 22 | var buf bytes.Buffer 23 | _, err = io.Copy(&buf, gz) 24 | gz.Close() 25 | 26 | if err != nil { 27 | return nil, fmt.Errorf("Read %q: %v", name, err) 28 | } 29 | 30 | return buf.Bytes(), nil 31 | } 32 | 33 | type asset struct { 34 | bytes []byte 35 | info os.FileInfo 36 | } 37 | 38 | type bindata_file_info struct { 39 | name string 40 | size int64 41 | mode os.FileMode 42 | modTime time.Time 43 | } 44 | 45 | func (fi bindata_file_info) Name() string { 46 | return fi.name 47 | } 48 | func (fi bindata_file_info) Size() int64 { 49 | return fi.size 50 | } 51 | func (fi bindata_file_info) Mode() os.FileMode { 52 | return fi.mode 53 | } 54 | func (fi bindata_file_info) ModTime() time.Time { 55 | return fi.modTime 56 | } 57 | func (fi bindata_file_info) IsDir() bool { 58 | return false 59 | } 60 | func (fi bindata_file_info) Sys() interface{} { 61 | return nil 62 | } 63 | 64 | var _chart_min_js = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xec\x7d\xfb\x7b\xdb\x36\xb2\xe8\xcf\x67\xff\x0a\x45\xe7\xc4\x21\x25\xe8\xd9\x75\xb6\x87\x32\xa5\x2f\x71\x9b\x6d\xef\x4d\xd3\x6c\x9c\xbb\x6d\xaf\xeb\xd3\x8f\x96\x60\x0b\x5b\x9a\xd4\x92\x94\x1f\xb5\xf5\xbf\xdf\x19\xbc\x08\x90\xa0\x2c\x3b\xe9\x63\xef\xe6\xeb\xd7\x98\x02\x06\x83\x01\x30\x18\xcc\x0c\x06\xc0\xa0\xf3\xe4\x4f\xad\x4e\xeb\x70\x19\x65\x45\xff\x1f\x39\x7e\x2f\x8b\x62\x15\x0c\x06\x73\x4c\xfa\x47\xde\x4f\xb3\xf3\x01\x26\xff\x9d\x66\x39\x4b\x93\xa0\x35\xea\x0f\xfb\x63\x48\xe1\xe5\xd2\xd5\x4d\xc6\xce\x97\x45\x6b\x3c\x1c\xed\xb7\xde\xb0\xf9\xcf\xad\x2f\xd2\xab\x84\x51\xcc\x7d\x47\x63\x1a\xe5\x74\xd1\x5a\x27\x0b\x9a\xb5\x8a\x25\x6d\x7d\xf3\xf5\xfb\x56\xcc\xe6\x34\xc9\xa9\xaa\x2b\x87\xca\xce\x59\xb1\x5c\x9f\xf6\xe7\xe9\xc5\x20\x49\x12\xc0\x32\x50\x14\x0d\x4e\xe3\xf4\x74\x70\x11\xe5\x05\xcd\x06\xaf\xbf\x3e\xfc\xf2\xcd\xd1\x97\xfd\x8b\x05\x14\x1e\xfc\xc9\x3b\x5b\x27\xf3\x02\x88\xf2\xfc\xdb\xf6\x3a\xa7\xad\xbc\xc8\xd8\xbc\x68\x4f\x2e\x23\xa8\x2d\x2c\x96\x2c\x27\x2c\x2c\xfa\x1c\x17\xa1\xa1\x06\x2f\xfc\x5b\xcc\xec\xcf\xa3\xe4\x32\xca\x01\x42\x7c\x10\x91\x58\x5c\x87\x05\x47\xc1\x8c\x12\x84\xf9\xb7\x19\x2d\xd6\x59\xd2\x2a\x8e\xdb\xe9\xd9\x59\x4e\x8b\x76\x97\x9d\xcc\xac\x5f\xc1\x22\x9d\xaf\x2f\x68\x52\xf4\x17\xf4\x2c\x5a\xc7\xc5\xdf\x19\xbd\xea\x9f\xd3\xe2\x30\xbd\x58\xad\x0b\xba\x38\x2a\x6e\x62\x0a\xf5\x63\xda\xdb\x2c\x5d\xd1\xac\xb8\xf9\x7b\x14\xaf\xa9\xc7\xfc\x0d\x90\xc8\x29\xb8\x62\x8b\x62\x19\x32\x4f\xd3\xd5\xfe\x0e\x53\xda\x3e\x49\x04\xc0\x92\x62\x9f\x5b\x10\x5f\xf1\xa4\xb6\x3f\x51\x49\x12\x0b\x25\x3a\x41\x96\x4a\x78\xdb\xac\xaa\xec\x32\x95\x5a\x2a\xe5\x27\xaa\x17\x10\x24\xca\x57\x74\x5e\xbc\x8b\xa0\x8f\x0c\x7c\x03\xa3\x38\xc9\xfb\x50\x80\x25\xd1\xd1\x3c\xc2\x96\x43\x8e\xcf\xfb\x79\x33\xa1\xaa\x93\xf2\xf0\xf6\x1c\x46\x39\x8a\x83\xdb\x28\x61\x17\x88\x2d\x09\x9e\x0c\x89\xfe\x71\x54\x50\x60\x93\xe7\x46\xca\x97\x51\xce\x92\xf3\xa0\x8d\xfc\xf5\xed\xba\xf8\xdb\x1a\x46\xb8\x4d\xf2\x65\x7a\xc5\xeb\xc1\xd2\x39\x7e\x7c\x7b\x49\xb3\x8c\x2d\x20\x61\x24\x12\x04\xaa\x64\x1d\xc7\xe5\x6f\xde\xbb\x56\x1a\x60\xe3\xa3\x62\x24\xbe\x66\x09\x3d\x4c\xe3\x34\x0b\xda\xd9\xf9\x69\xe4\x0d\x09\xfe\xd7\x1f\xf9\xed\x32\x5f\x20\x52\x35\x01\x31\xaf\xa3\x53\x1a\xe7\x9a\x1a\xfe\x33\x68\x1f\x3c\x0d\x2f\x11\xfb\xd3\xa9\x2c\xfb\x75\x52\xd0\x73\x98\x5f\xdf\x26\xf1\x8d\x06\x7e\x49\xcf\x59\xf2\xa2\xf8\xbf\x34\x4b\x35\xf5\xaf\xd2\xa4\x78\x15\x5d\x30\x00\x6b\x3f\xfb\x8a\xc6\x97\xd0\xb5\xf3\xa8\xf5\x86\xae\xe9\x33\xd2\x2a\x53\xf0\xc7\x8b\x8c\x45\x31\x7c\xe4\x51\x92\xf7\x72\x9a\xb1\xb3\x76\x89\xe4\x88\xfd\x42\x83\xd1\xd8\x48\x40\xc6\x0c\xda\x49\x9a\x5d\x44\xb1\x01\x28\x9b\xfc\x9f\xcf\x9f\x3f\x6f\x93\x8c\xe6\xab\x34\xc9\xd9\x25\xef\xcf\x8b\x88\x25\x05\xfc\xff\xa2\x64\x02\x4e\x3c\xb4\xfb\x7d\x9a\xc6\x05\x5b\xf1\x96\xcf\xd7\x79\x91\x5e\x94\x29\x23\x52\x88\xef\x2f\x2f\x61\xa2\xe4\xc1\x71\xfb\x22\x85\xc9\x7b\x91\x5e\xd2\x36\x69\x17\xe9\x7a\xbe\xcc\x0b\x3e\x9c\xe2\x87\xcc\xe0\x40\xe9\xba\x68\x9f\xa8\xf2\xaf\x58\x1c\xd7\x47\x64\xd8\xff\x1c\x86\x44\x81\x7c\x60\x7f\x19\x68\x44\x8f\xfd\xd9\x4a\xb2\xfb\xcc\xc8\x51\xbd\x76\x76\x56\x22\x79\xcf\x8a\x0f\x1f\xc0\x2a\xae\x0a\x55\x65\xba\x20\xed\x34\x8d\x17\xf5\x42\x2e\xea\x7e\x78\x1b\x2d\x16\x38\xa5\x9e\xab\x94\xef\x6b\x29\x87\x11\x4c\x65\x5e\xe3\xe7\x3a\x29\xcd\x12\x9a\xbd\x8b\x16\x6c\x9d\x1b\x45\xbf\xe5\x12\x31\x18\x0d\x75\xdd\xf4\x62\x15\x47\x05\x45\xe6\x67\x67\x2d\x2f\xc6\x99\xe0\xdf\x3e\x9d\xc2\x5c\xe0\xdf\x4f\xa7\x41\xeb\xe0\xe9\x86\x27\xb4\xf8\xec\x68\xe1\xf4\xb8\x00\xf1\xc0\xde\xd7\x71\x34\xc1\xfc\x6f\x7a\xf3\x32\x9a\xff\x7c\x9e\xa5\xb0\xe6\xa8\x26\xa6\xc9\x0b\x25\x37\x40\xe6\x9e\x03\x17\xe7\x81\xb1\x74\x6c\x4c\x00\x14\xd4\x31\x85\x4a\x4c\x80\x0d\x88\xe6\x7e\x71\xb3\xa2\x20\xa9\x36\x5c\x7c\xe6\x21\x05\x01\x17\x83\xfc\xc6\x24\x90\x98\x79\x9f\x46\xf3\xa5\xb5\x60\x10\xea\xdf\x0a\xd8\x17\x59\x16\xdd\xf4\x57\x59\x5a\xa4\x88\xa5\x9f\xe3\x12\x08\x62\x35\x8e\xbd\x28\x3b\xe7\x0b\x46\x4e\x3e\xf3\x27\xec\x0c\x96\x06\xfc\xa7\x1f\xd3\xe4\x1c\x04\x73\x18\x76\xd5\xb7\xc0\x95\x4c\xce\xd2\xcc\x4b\xc2\xe1\x24\x39\x50\x39\x93\xa4\xdb\xf5\x59\x3f\x5a\xad\xe2\x1b\x8f\x92\xe3\xe2\x38\x39\x21\xc9\x09\xac\xa6\xc9\x3c\x2a\xbc\xdc\xf7\x37\x20\x84\x68\x0b\x4b\x22\x8e\xb4\xc5\x40\x7c\xdb\x25\xd2\x13\x92\x5a\x25\x48\x0a\x4d\x9a\xc7\x69\x62\x2f\x9b\x62\x59\x84\x4e\x90\xab\x00\x36\x54\xe7\x53\x92\xc3\xc2\xda\x5f\x46\xf9\xb7\x57\x89\x5a\xdd\x00\xdb\xde\x9e\xc7\x8e\xf3\x93\x90\xfa\x1b\x9f\xb0\x0d\x89\xb0\xb7\xae\x0b\x9a\x2c\x2c\xdc\x1a\xe5\x4e\xdd\x35\xf2\xcb\x8a\x61\x6d\x86\x7f\x2a\x84\x30\x27\x21\x85\x22\x04\x48\x29\x36\x64\x09\xa4\x5c\xd0\xec\xdc\x68\xa5\x68\x64\xb1\xdb\xa0\x0d\x7d\xbd\x1e\xf6\xd7\x49\xbe\x64\x67\x85\x77\x0b\xa8\x23\xd9\xb9\x7c\xfd\x28\xa0\x3b\x63\xa8\x88\x81\x1a\x74\xfd\xed\x59\x45\xab\x80\xf1\xae\x56\x25\x01\x7d\x8d\x5a\x26\x40\x43\x27\x6a\x14\x29\xf0\x00\x2d\x79\x80\x22\x0f\x00\xe7\x1c\xd3\x13\xe0\x1a\xa6\x8a\x52\x49\x5e\x6f\xb4\x21\x59\xe8\xc1\x02\xbd\xa4\x19\xad\x50\x20\xd0\x1d\x9f\xa8\xa6\x08\x5e\x36\x47\x16\x46\x07\x94\x0d\xe8\x3f\xda\x5f\xad\x73\xc8\xc2\xee\xa3\x1b\x58\xdc\xcf\x80\xb4\x37\x30\x98\xdf\xd5\xf0\x22\xfb\xd3\xbb\x3b\x8f\x86\xbd\x51\x49\x36\x4c\x9c\xee\x68\x92\x97\x84\xe7\x40\xb8\x60\xed\x10\xc7\x06\xe7\x00\xf3\x12\x5f\x35\x20\xd9\xa8\x5a\xde\x66\xf4\x92\xc1\x22\xb0\xad\x26\x3d\x59\xcc\xfa\x7a\x50\xdf\x14\x7a\x2b\xef\xf5\xee\xaf\x89\x25\x80\x9e\x81\x4a\x52\xe7\x7a\xae\x4f\x42\x1d\x7b\x7b\x35\x1e\x6f\xc3\xcc\x01\xe5\x73\x3d\x2f\xd2\xac\xed\xcf\x8a\xbe\xf1\xdb\x94\x24\xb2\x2a\x35\xf5\x38\x46\xcd\x4c\xc0\x25\xb9\xc9\x86\x42\x1b\x2d\x11\x85\x74\x53\x8e\x90\x66\x96\x90\x95\xdf\x20\xa5\xca\xf4\x84\x5e\xb5\x80\x5e\x35\xd3\x32\x02\x84\x47\x9e\x01\x01\x7c\x09\xd9\x3f\xfd\x94\xaf\xa1\x11\x3f\xfd\x64\x23\x82\xf1\x9d\x03\xcb\x26\x69\xba\x0a\x2d\x59\xb9\x86\xd4\x35\x5b\xd4\xe7\xcb\x50\x11\x57\x6b\x6f\x9b\xdb\x12\xbd\x76\xb7\xe8\x76\x37\x1b\xcf\x27\x0b\xc0\x71\x15\x65\x89\xd5\xc9\x57\x30\xc6\xe9\x15\x6f\x70\x1a\xd3\xbd\xbd\xb6\xca\x6c\x87\x21\xd2\x94\x9e\xb5\x6c\x10\x8e\x62\x6f\xcf\xfc\x85\x8c\x49\x56\x80\x3d\xba\x58\x84\x0e\x04\xa0\x70\x82\x9a\xb6\xb7\x27\xfe\x22\x14\x39\xc3\x89\x99\xbf\x59\x5f\x9c\xd2\xcc\x21\x8e\x9e\x40\x5e\xf4\xc6\x5b\x45\x59\x4e\x5f\xc5\x29\x48\xc7\xc2\x87\x79\xc0\xf2\x57\x2c\x61\x05\xe5\x15\x9e\xa3\x10\x89\xae\x5d\xc2\xec\x9b\xa8\x58\x62\x9e\x1c\x70\xfc\xc9\xc5\xc1\x05\x16\x61\x49\x73\x11\x96\x54\x8b\x5c\xe2\xf4\x9d\x47\xab\x1a\xeb\x03\x1f\xa3\x64\xe0\x1f\xc5\x54\x4f\x7d\x26\xa4\x3e\xcf\xa5\x38\x75\xa7\x45\x55\x2a\xb4\x0a\x64\x79\x30\x45\xbe\xa0\x73\x58\xff\xe2\xb7\x71\x34\xa7\xb9\x8b\xa8\xe2\xe9\xe8\x49\x18\x0e\xf7\xf6\x70\x85\x02\xfe\x2e\xd2\x23\xb0\xb5\x92\x73\xcf\xef\xe7\xab\x98\x15\x5e\xbb\xdf\xf6\x8f\x47\x27\x72\xfe\x05\x43\xe0\x9f\x23\x68\x63\x06\x9a\x01\xa8\x2f\x4e\x94\x1d\xde\xb6\xfe\xdb\xaf\x07\xa3\xcf\x87\xd0\xc0\x6b\x6c\x20\x10\xf3\x22\x39\x07\x3d\x25\x4b\x2f\xde\xa6\xa0\x60\x3a\x65\x15\xeb\x5f\xf7\x8a\xfe\x35\xcc\x17\xd6\xbf\x81\xaf\x1b\x58\x81\x39\xb2\xfc\x9f\x59\xe1\xd1\x0e\xed\xe6\x1d\xb0\x33\xd2\x70\xdc\x91\x75\x74\xf9\xdf\xa8\x88\x92\xb1\x07\xb3\x42\x8b\xed\xe1\x14\xd8\x61\x38\xcd\x61\x69\x48\xbb\x25\xb8\x4f\xc0\x14\x01\x32\x82\x94\x2c\x18\xa8\xa3\xc9\x1c\x6c\x01\x2e\x20\xa2\x98\x45\xf9\x5b\x76\x4d\x63\x77\x3f\x8d\x41\xfc\x0e\x67\xc3\xa0\xbf\x0f\x5d\x70\x83\x4d\xc2\x0e\x02\xdb\x61\x9d\x5d\x56\xc5\x16\x2e\x53\x42\x20\x95\xc4\xf3\xaf\x55\x7a\xe5\xa9\x36\x8e\xfd\xae\x91\x26\x5a\x3b\xf6\xb1\x71\x8e\x42\x14\x0a\xb1\x4a\x21\x0a\x85\x98\x2c\x04\x4b\x6f\x27\x19\x78\x49\x37\xf5\x71\xed\xeb\xa4\xe2\x5b\xf6\xc6\x2d\x4b\x40\x9b\x0b\x6e\xaf\x03\xac\x3c\xea\x70\x74\x40\x03\x34\x24\xc0\xaa\x79\x0a\xa7\x00\x15\x04\xb0\x67\x15\x6c\x77\x59\x85\x15\x29\x12\x96\x77\x1c\xac\x9c\xf3\x35\xea\x6f\xdf\x66\x0b\x9a\x7d\x7b\xf6\x4d\x74\x0e\x33\x68\xbd\xa0\x8d\x93\xe0\x2c\x4e\x41\x8a\xf3\xcf\x38\x3d\x87\xdc\x01\xff\x7e\xfd\x66\x34\xc4\xf5\xe7\x50\x4c\x08\x89\x95\xdb\x7d\xef\x60\xd0\xea\x9d\x4c\x12\xd1\xcd\xc0\x0e\xd0\x01\x06\x6a\x36\xf0\x46\xfd\xfd\x0e\xf5\xb1\x33\xd2\x69\x18\xc1\x32\x8d\xf5\xc0\x52\x79\x01\x7f\x26\x31\x8c\x65\x06\x9c\x11\x77\xc3\xfe\x3e\xc9\xa6\xf0\xef\xde\xde\x93\x7c\x96\xf5\xe0\x2b\xe0\xa9\xe5\x4a\x33\x17\x98\xa3\xd3\xdc\x8b\x7b\x99\x0f\x82\xf2\xc6\x9b\xa3\xac\xe3\xc9\x73\xca\x62\x2f\x86\xfa\x3a\x7a\x5c\x40\x3b\x5e\xfb\xbe\x5f\x49\x40\xf1\x05\xec\x63\x50\x99\xed\x52\xea\x2c\x5c\xf4\x56\x20\x23\x2a\xc9\x47\x22\x81\xab\xc3\xde\xd9\xe0\xd2\x9f\x78\x47\xd3\xe8\xee\x2e\x9a\x8e\x3b\x47\x20\x14\x9e\x2c\x27\xa8\x36\x40\x9a\x7f\xd9\x81\xee\xa9\xc1\x93\x23\x35\xf3\xbd\x65\xf8\x04\xd4\x1d\x25\x54\x40\xf2\xae\x61\x55\xe5\x52\xe7\x72\x30\x16\x50\xfe\x69\x46\xa3\x9f\x27\x97\x03\x17\x2a\x21\x8f\xdc\x79\x6a\x3a\x2e\xa1\x9e\xa3\x30\x85\x86\x9c\x0d\x8e\x60\x12\xe6\xdc\x5c\x3f\x22\xf8\x57\x58\xe4\x97\x04\x64\x63\xb0\x02\xeb\xf3\x3a\x58\x75\x8f\x3a\x97\x9c\xb9\x0a\x69\x1b\x54\xa4\x85\xfa\xd5\xa2\xa6\xf4\x18\xfc\xf8\xdd\x00\x4a\xe4\x28\xcb\x67\xb8\x4c\xbe\x52\x85\xda\xe9\xe9\x3f\xc0\xd6\x44\xb0\x15\x28\x44\x64\x95\x59\x12\xc8\xbf\x5d\x71\xfd\x47\x8a\xe6\x95\xb1\x76\x4f\x36\x93\x2b\x56\x2c\x3d\x40\xa0\xa0\xbc\x67\xb0\xe2\xf5\x33\xba\x42\xa9\xea\x0d\x8e\x7f\xcc\x7e\x2c\x7e\x4c\x4e\x06\xe7\xa4\xdd\x6a\x6b\xb9\x79\xf0\x14\xbe\xff\x01\x92\xce\x6b\xff\x07\x7c\x69\x70\xcf\xfb\x9f\xbb\xa7\x53\xff\xf8\x7f\x7e\x2c\x4e\x3a\xfe\x33\x2c\xf5\x5f\xa3\x1f\x33\x13\xe4\xc7\x22\xf4\xfa\x9d\x99\xff\x74\x8a\xb9\xcf\xc8\x7f\x8d\xc8\xb3\x12\xf1\x7f\x68\xbc\xcf\xfc\x49\x99\x0c\x16\x93\x4a\xd7\x64\xea\x4c\x8e\x5f\x64\xfe\xf8\x23\xa4\x77\xb1\xec\x46\x8e\xcd\x4a\xe4\x3c\xe3\xe8\x82\xfc\xb8\x38\x09\xf1\x1f\x35\x74\x6c\x86\x4e\xac\x80\x6e\x70\x19\x02\x13\x43\x48\x4d\x58\x74\x55\xef\x6a\x75\x16\xd5\x58\xa1\x9b\x95\x86\x84\x18\x20\x98\xd5\x57\x62\x15\x00\x39\x04\xc3\x29\xfc\x27\x0d\x52\x33\xe5\x2a\x0e\xd7\x9d\x11\xa3\x44\xc4\xad\x49\x65\x2a\x8a\x45\x6a\x6f\x2f\xf1\x52\xc3\x5a\x40\x79\x90\x82\x91\x14\x1e\x02\xc2\x5b\x6e\x48\x06\xb8\x62\x80\x28\x1c\x09\xcb\x20\x45\x9e\xa2\xdc\xc7\xf4\xe5\xd9\x19\x9d\xa3\x8b\x0a\x85\x78\x64\x68\x73\x86\xd0\x07\xf3\x30\xca\xe9\xd7\xc9\xdf\xd6\xd1\xc2\x09\xd0\x91\x20\xc2\x55\xe5\x82\xe9\x8d\x3a\xb0\x20\x16\xbd\xb1\xaf\x90\x35\xc3\x7a\xc5\x00\x25\xcf\xc1\x68\x06\xc2\x0b\x70\x07\xbd\x21\x7c\x78\xbd\x9e\xc4\x00\xfa\xb6\x42\x72\xb8\x3e\x65\xf3\x06\x92\x0c\xa2\x1a\xc1\x46\x1d\xcf\x2b\xc2\x62\x30\x02\x9c\x58\xa2\x3b\x32\xe9\x6b\x2a\x56\x25\x10\x48\x44\x02\x81\xb8\x70\x2c\xd0\x94\xcd\xe4\xae\xbb\x46\x0a\xed\x8e\x73\x02\xf6\xaa\x34\x76\x8a\xde\xa8\xd2\x8b\xce\x82\x75\x2a\x75\x57\x96\x94\x76\xcc\x21\xf9\xdb\x1a\xc4\x41\x43\x37\x01\x32\x55\xbd\x45\x74\x73\x89\x0a\xd1\xd5\xce\x6d\x2a\xea\x22\xbb\xd6\xc1\x95\x4e\x3e\x02\xde\x75\x77\x9d\x58\x9c\xd2\x1c\xb0\x8e\x4a\x85\x0c\x54\x85\xee\x48\x37\xa2\xa1\x74\x4b\x96\x86\x59\x52\x2b\x6d\xb4\xa3\xa9\x6e\xde\xd1\xba\x7a\x59\xb6\x03\x88\x4c\xfe\xfd\xf2\x7a\x95\xba\xaa\x1e\xc2\xca\x5c\xcc\x46\x81\xb1\x30\x8e\xc9\x68\x88\xc3\x80\x3d\xea\x6b\xda\x9b\x10\x8c\x34\x02\xaf\x67\xa0\xe8\x01\x0e\x24\xc1\x1e\x8a\xed\x54\x0c\x03\x85\xcc\x1a\x9a\x2a\x61\x48\x16\x1f\xa5\x6a\x7d\x30\x6f\x7d\x63\xac\x0e\x59\xe6\x9e\xb1\xd3\x70\x34\x03\x06\x55\x1d\xcd\x15\xbf\x51\x4f\x31\x5e\xd9\x6b\x38\x31\x1b\x70\xa8\x11\x53\x45\x35\x03\x5a\xb3\xda\x5d\xb8\x6c\x9c\x31\x74\x12\x53\x21\xea\x0f\xaa\xe9\x8a\x21\xcd\xfe\xfc\x32\x8e\xf2\xa2\x22\x36\x84\x81\x3d\xea\xff\x65\x38\xda\xff\x1c\x6c\xec\x21\x68\xf6\xa3\x49\xbd\x97\x45\x63\xb1\xa7\x85\xbd\xdf\xff\xcc\x27\xf9\x81\xd6\xbb\x20\xcb\x83\x82\x84\x85\x74\xf0\x67\x3f\xc0\x3f\x5e\xa9\xd2\x8b\x0f\x14\xea\xde\x68\x00\xd6\x41\xcf\xcb\xeb\x83\x04\xe8\xfd\x92\xad\xc7\x1d\x50\xbd\x8a\x1e\xf3\x15\x92\x01\xe8\x8a\x06\x73\xfd\x51\x9a\x62\x35\x84\xf3\xf0\x3d\x8d\xa8\xf0\xf7\x63\xdb\x31\x16\xed\x00\xb6\x30\x1b\xd2\x01\xa5\xfa\x03\x1a\x33\x9a\x16\xb3\x1e\x70\xd2\xa3\x46\x27\xa8\x75\xc5\x4e\xc5\x3a\xfd\x7d\xa3\x4b\xd0\x85\xdc\xdc\x19\x93\xba\xd0\xf7\x3c\xd6\xc5\x8f\x1e\x2b\x99\x63\x67\x24\xe6\x3a\xa0\x10\x75\x59\x65\x88\x76\xc2\x56\x5b\x1a\x3c\xc0\xd7\x01\xa0\xfd\xf1\xbe\xaf\x08\xb4\x57\x8a\x1a\x04\xd6\x5c\x0a\xa3\x97\xa0\xa1\xcf\xdd\xc2\xbf\x77\xd5\x57\x2d\xe5\x40\x28\x06\x8c\xd6\x37\x15\x14\x7d\x76\x30\x1a\x8c\xfb\x7f\xd9\x9f\xfd\xa5\xbf\xff\x7c\x2c\x94\x98\x31\x4f\x81\xb1\x87\x2e\x91\xc9\x7c\xe8\xfa\xfb\x3c\x03\x49\xc3\x3f\xc1\x58\x26\xd4\x20\xc7\xfd\xb1\x01\xfa\xdf\x9f\x21\x70\x15\xe2\xb9\x05\xf2\xf9\x9f\x11\xc8\xec\xe5\x26\xaa\xfb\x58\x1d\x74\x9c\x68\xb3\xea\x17\x60\xa5\x82\xf7\x67\xb5\x2b\xc6\x5c\x07\xe9\xf6\xf7\x37\xa0\x4c\x9e\xa2\x1f\x84\xfe\x73\x0d\x06\x07\x6e\x33\xbc\xca\xa2\x0b\xcb\xdd\x2c\x3b\x54\x3a\xb7\x0c\x48\xbe\x21\xc1\xc1\xef\xee\x64\xee\x15\x3d\xfd\x99\x15\xef\xb6\xc2\x5c\xa4\xbf\x6c\x07\x48\xef\x29\x9f\x37\xe4\x3b\xb8\x40\x16\xc9\x69\xf1\x9e\x5d\xe0\xf6\x1c\x68\xd4\x23\xfa\xd9\xe0\x39\x58\xe8\xe8\xe9\x7b\x1b\xf2\x5d\xf6\x39\x8d\x77\x68\x7a\x09\xd8\xd8\xf2\xc3\x6d\x20\xd0\xf0\xad\xf9\xe9\xf6\xd2\xb9\x3b\xbb\xb9\xd5\xf3\x18\x0c\x02\x77\xbb\x5f\xa3\x15\xa3\x77\xab\x5f\x5b\x5e\x54\xed\x95\x20\xa9\x98\xc6\x11\x08\xd7\x65\x78\x75\x4c\x4f\x80\x98\xbe\xb0\x33\x48\x6c\xf6\x53\xd4\xed\xca\x5d\xfb\x68\xc0\x48\x16\x2e\x3d\x2a\x76\xfa\xe3\x18\x2c\x9b\x0c\xd0\x45\xbe\x70\xb1\xc8\xdf\x3e\x61\xd3\x68\x96\x96\x24\x88\xae\x3f\xf5\x62\x3f\x50\x9e\xc5\xd4\xdf\x4c\x30\x41\xfa\x00\xdf\x51\xb0\x98\xd8\x25\x7d\x9b\xe6\x0c\x4b\xd4\x3d\xe0\x48\x75\x58\xf4\xd3\x8c\x9d\xb3\x24\x8a\xf9\xfe\xed\xdd\x5d\x81\xf1\x02\xfd\xf9\x3a\xcb\xe0\xe7\x7b\xb0\x88\x29\x26\xf6\xf3\x6c\xfe\x65\x4c\xd1\x38\x26\x60\xa8\x61\x05\x38\x37\x70\x43\xf1\x30\x66\x90\xfa\x0e\xac\x2a\xcf\x2f\x1d\xda\x7c\xaf\x97\xe6\x33\x8f\x85\xfa\xc7\xf1\xf0\x04\x3a\x19\xa1\xbf\xef\xa5\xfd\x98\x9e\x61\x14\x87\x23\xf7\x07\xc8\x2d\xd2\x95\x1f\xf0\xc2\x8e\x12\x36\x18\x41\xa7\x15\xb9\x01\x53\x95\x3b\xf4\x16\x0b\xde\x92\x9a\x5f\xb5\xd0\x59\xaf\x59\x5e\xa0\x45\x3a\xab\x27\x79\x08\x1a\x40\x7a\x51\x44\xf3\x25\xcf\x9a\x59\xbf\xbc\x76\x9a\xb4\xbb\x02\xea\x58\x7c\x9f\x84\xe8\x5e\xff\x99\x8b\x05\xdc\xdd\x6e\xaa\xde\xc8\x35\x28\x70\xa4\x22\x11\xe4\xc9\x08\xe9\x58\x50\x93\x0e\xe3\x97\x93\x8e\xf9\x86\xbc\x42\x4e\x3d\x05\x86\x16\xdb\xf1\x0e\x32\x28\xcf\x80\xb5\x5d\x7d\x86\xb8\xc3\x65\x6d\xb8\xb1\x12\xee\x18\xd0\x1a\x9c\x4b\xd5\x56\x87\xb5\xcf\xf1\x1a\x23\x57\x78\x84\x8f\x8c\x5f\x61\xc4\x28\x8f\x36\x38\x0c\xcc\x3a\x69\x20\xab\xb2\xdb\xc7\xc9\xfc\xb9\x8a\x91\x72\x77\x02\x10\xfa\x2e\xe4\xec\xfd\x4d\x74\xcd\x2e\xd6\x17\x3c\x44\xc3\xb5\xb9\xd3\x5f\x45\xc8\xc0\x6f\xd2\x85\xf6\x8e\x33\xc9\x37\xbc\xcc\x86\xbc\xb7\x10\x89\x80\x9b\x07\x62\x12\x85\x36\xe4\x85\x74\x75\x4b\x5c\xb8\xa7\x5e\xa7\xd2\x0e\x9d\x71\xd6\x34\x2f\xae\x71\x47\xca\x0e\xdf\xc9\x6b\x21\x3b\x52\x5e\x2d\xe8\x25\x9b\x53\xee\xbb\xe6\xd1\x1a\xb8\x2f\xab\x20\x73\x0c\x18\x50\x31\x43\xdd\xf6\xea\xba\x4d\x2a\x79\x32\x1c\x28\xaf\x64\xaa\xe4\x4e\x43\x25\x25\xa4\x44\xbe\x05\x90\x47\x9d\x78\x4d\xf9\x0d\xe9\x3e\x0e\xf2\x37\x7c\x92\x83\xdc\xb4\x83\xbc\xb0\x83\x44\x3a\x17\x37\x18\x1f\x52\xc8\x5e\x2a\x24\xe5\xc0\x6a\xdf\x41\xe1\x33\x1e\x34\x81\xde\xa2\xda\x0c\x50\x63\xd8\x6d\xb7\x70\x0b\x0b\x9a\x0f\x7f\xe9\x86\xfc\x02\xc5\xe2\x34\x39\x87\x95\xf2\x3d\xbd\x76\x4d\x60\x44\x1a\x32\xe9\xeb\x1a\x96\x7b\xe6\xd4\x9a\x39\x32\x1c\xab\x7f\x01\x3a\xc4\x3a\xa3\x88\x0b\x92\x05\x99\x93\x3c\xa4\xd3\x7c\x46\x83\x1c\x1a\x99\x6f\xc8\x4b\xa8\x73\x91\x45\x57\xef\x50\x96\xd2\x05\xb6\x8a\xef\x53\x34\xac\x2e\x45\xff\x14\xa3\x88\xde\x82\x9e\x0b\x8b\x12\x54\x01\x92\xe3\x7d\x0a\x3a\x66\x8a\x4b\x44\xc1\xd7\x1a\xfe\x3b\xef\xc9\x94\x7f\xae\x23\xc0\x0f\xc6\x00\xdf\xaf\x10\x79\x38\x97\xf0\x5f\xdc\x34\x30\xcb\x40\x4a\xd2\x4b\x9b\x4b\x75\x13\x22\x31\x77\x13\xab\xa4\x4e\xa9\x97\x13\xa5\x4a\xcc\xaa\x8c\xaa\xdd\x55\x82\xe8\xf6\xcc\xe3\x34\xa7\xa2\xb5\x18\x5f\xa6\x9c\x90\x3c\x46\x83\xf6\xdf\xe3\xb6\xa8\xd5\x51\xb9\xdc\x65\x4d\x57\x98\x94\x87\x4c\x86\x00\xa2\x20\x09\x65\x3c\x20\x5b\x84\x6b\x0f\x77\x4a\x35\xb6\x63\x99\x7e\x22\x43\x0c\xfb\x65\x5c\xd4\xde\x1e\xcf\x83\x04\x98\xd2\x9e\x2f\x31\x24\xb0\xa0\x46\x31\xa4\x88\x15\x9a\x97\x42\xc6\xc3\xed\x58\x24\xca\xd8\x78\xbd\x2d\x81\x1d\x1b\xc7\x3c\x70\x8e\x70\x86\x76\xe4\x7e\xe3\x95\xd4\xcb\x20\x3b\x92\xc3\x82\xe7\x00\x7d\x2b\x40\x6d\xf5\x40\x95\x11\xd4\x07\xb5\x70\x49\xc4\xe5\xf9\x93\x72\x37\xbc\x22\x71\xc3\x77\x5e\x2d\xd5\x47\x71\x64\x74\x71\xdf\x11\x35\x36\xa3\x03\xa3\x9c\x11\x53\x18\xbc\x77\x20\x2c\x65\xaa\x0c\x5e\x2c\x21\x54\xc8\x23\xd3\xb1\x8b\x65\x9e\x92\x53\xe4\x85\xd5\x4b\x8e\x4d\x62\xbe\xc1\x6f\x6c\xd0\xef\x18\x8e\x52\xf6\xde\x59\x9c\x5e\x05\x73\xf8\xc0\xb0\x57\xa7\x2b\x47\xb3\x09\x82\x2a\x36\x51\x3d\xa4\x07\x65\x6f\xef\x49\x31\xab\xa8\x91\x82\x78\x14\x01\x0d\x85\x78\x80\x63\x43\x9e\x88\x9a\xb4\x33\x1d\xc1\x51\x8d\x00\x2a\x38\x8a\x03\x80\xb6\xa5\x69\xa9\x36\xc1\x51\xa6\x64\x7d\xdd\x53\xda\xcb\x4f\xe1\x6b\xe1\xe0\xd2\x43\xcf\xc2\x1a\x73\x38\xe5\xdb\x17\x44\x6c\xc8\x02\xe4\x6f\x96\xde\x04\xb5\xc8\x09\x9c\x25\x40\xd8\x2b\x39\xdf\x30\x4d\xa8\x18\xbe\x11\x20\x6c\xf1\xd6\xa4\x68\xe0\x29\xbd\x5a\xd4\x39\x0a\xb2\xc4\xfa\x28\xf4\x32\x15\x12\x32\xf3\xdc\xe9\x5e\xfb\x4a\xc6\xf2\x36\xe5\x2f\x65\x24\x2f\xf6\xaf\x05\xf2\xa2\x80\xf5\xe9\x74\x5d\xd0\x26\x1c\x06\x80\x46\x02\xdd\x83\xbd\xdf\x72\x89\xaf\x8d\x19\xab\x19\xd8\x1a\x56\x1b\xd7\x17\x0c\x92\x58\x18\x13\x83\x8b\x8c\x39\x1a\x09\x52\xb5\xc7\x6d\x73\x47\x6a\x78\x7c\x22\xfa\x38\x75\x68\x2d\x4f\x46\x65\xc8\x94\x88\x16\x78\x12\x86\x0e\x24\x32\x73\x06\x25\x86\x81\x67\x05\x9a\x15\x7c\x79\x75\x17\x03\x2b\x0a\xf5\x1a\xdc\xab\xdc\x08\x16\x41\x45\xd0\x12\xbc\x18\xf9\x93\xde\xdd\x89\xd8\x2b\x17\xf9\x05\x69\xe4\x6c\x3b\x9a\x55\xce\x63\x77\xa6\x07\x1a\x39\x51\x6d\x9c\x0e\x7d\x55\xd9\x22\x2a\x22\x30\x98\x55\x61\xf5\x53\x01\x8e\xfc\x5b\xb5\xa5\x1c\x91\x25\x18\x7b\x2e\xa8\x1e\x74\xe2\x94\xef\xca\x46\x76\xfe\x71\x76\xd2\x5f\x61\xdc\x04\x28\xec\xb5\x8c\xd3\x28\x73\x25\xe7\xf4\x5c\x08\xb1\x65\x08\x22\x8d\x14\x60\x58\xf9\xa4\x87\x4e\xed\x25\x48\xdb\x5e\x4f\x8c\xe5\x1c\x37\x44\xd7\xf8\x4f\x3d\xf8\x87\xeb\x1c\xa0\x71\x90\x08\xf3\x63\xfc\x27\x73\x04\x94\x99\x15\xdb\xc6\x03\x06\x1f\x49\xaa\x99\x24\x93\x95\x74\x15\xc7\x4b\x18\x54\xfc\x17\x03\xaf\x44\xbc\xbc\xbf\xb7\x17\xc9\x60\x34\x48\x47\x1d\x50\xd6\x12\x59\x61\x6b\xb1\x84\xc1\xc0\x84\x4c\x7d\xdf\xf8\x64\x2e\xbe\xcb\x9d\x63\x5b\xd4\xb8\xe2\x4e\x81\x73\x7c\xb2\x16\xe5\x6e\xcf\x58\x1c\x83\xb9\xf5\x53\x1e\x5d\xd2\x45\xff\x4c\x85\x22\xa3\xe5\xab\x7f\x10\x14\x4c\x3f\xd3\x12\x4c\xfc\xd6\x80\xc6\xcf\x8d\x66\xd6\x34\xbc\xf0\x32\x50\x37\xc2\x73\xfc\x93\xc0\xaf\x18\x38\x18\x7e\xc5\xdc\x6e\x4d\xa6\x55\xd9\x34\x18\xcf\x12\x6e\xce\x7a\x69\x97\xfa\x83\xf1\xc6\x64\x76\x18\x40\xdc\x24\xa5\xfd\x6f\x8c\x06\x79\x80\x67\xd1\xbf\x86\x22\x8b\xfe\x0d\xb9\x56\x71\xbe\x56\x07\x54\x82\x80\xc9\xcd\x36\x28\x15\x3c\x4c\xae\x65\xe0\xaf\x13\x95\xc8\x23\xba\x7b\x9c\x50\x3a\xa8\x9b\x14\xa0\x07\x6f\x81\x53\x71\xcc\xe4\xac\x0c\xa9\x6e\x02\x14\xd9\xe4\x4c\x07\x46\x37\x01\xf2\x5c\x01\x87\x1a\x50\x23\x18\x64\x92\x02\xc3\xa9\xdf\x6f\x25\xd2\x8e\xb8\x16\x25\x5e\x6d\x27\xb7\x12\x25\x5e\x96\x69\x26\xdc\x0e\xfa\x36\x4a\x34\x35\xc1\x8a\x1e\x27\x73\x33\x84\xdb\x05\x6e\xc6\x78\x93\x58\x9c\x68\x98\x13\xb1\x18\xf3\x86\xe5\xc1\xda\xfc\x69\x04\x60\x37\xce\x29\x2b\x4e\x5b\x90\x1c\xa0\xdc\xe9\x73\xfc\x84\xb3\x77\x50\x72\x3a\x01\x3b\x2e\x30\x97\x6b\xb0\x7b\x85\xb0\x0d\xb6\x08\xe0\x8d\x2f\x45\xb8\x88\x3c\x49\x2a\x11\xad\xca\x88\x96\xcd\x54\x7e\x2f\x4f\x4d\x19\x63\xb6\x18\xd1\x2a\x4c\x84\x38\x59\x29\x20\x4f\x3e\xe6\x2c\xfa\xff\x65\x86\xcc\xf1\xbc\x80\x70\x79\xb8\x19\x4b\x9d\x27\x78\x20\x0f\x62\xbb\x83\x8a\x6e\x58\xd4\x64\xb5\x83\x89\x1e\xc0\x31\x42\x1a\x9b\x87\x9d\x36\x90\x96\xbe\x84\xb5\xeb\xf9\x9f\xbf\xbe\x88\xce\x9b\xcc\x33\x4b\xa1\x04\xb2\xbe\x80\xf5\xee\xff\xbc\x7b\x6d\x18\x14\xb6\xd5\x64\xb8\xc5\x30\xa6\x5a\xd8\x83\x8e\xe8\x78\x23\x06\x39\x77\xb8\xd0\x9b\x42\x8a\x51\xd7\x31\xa3\x85\xc1\x60\x2e\x7f\xf9\x60\x81\xe6\x76\x40\xb0\x8e\xcc\xb7\x28\x01\x35\x26\xe1\xce\x70\xa3\x30\x4f\xd1\x01\xd5\x0d\xf9\xa0\x0b\x94\xc7\xbc\x8e\xab\xb9\x27\xb3\xd4\xdb\x96\xed\x07\xb7\xe6\x31\x31\x0c\xd9\x89\xbc\x98\xe8\xd3\x75\xb9\xaf\xce\x65\x60\x56\x6e\x86\x3f\x63\x42\x3d\x72\x34\x45\xcf\xb9\x2e\xdd\x17\xa7\xce\x88\x55\x03\x61\x77\x77\xb7\x1b\x3d\xf2\x3c\x8a\x1a\x10\xa4\xd2\xdc\x10\xa2\x64\xe1\xb5\xdf\x00\x81\xad\x24\x2d\x5a\x50\xe5\x25\x5b\xd0\x05\x9e\xaf\xe0\x1c\xd0\x12\xdc\xd6\xca\xd3\x16\x2b\x5a\xa0\xb0\x24\xcf\x8a\xd6\x29\xa5\x49\x2b\xa3\xe7\xe8\xb1\xcd\x40\xa9\x2e\xcd\x58\xf4\x4c\x48\xcd\xd3\x1a\xf0\x48\xa9\xab\x35\x27\x82\x73\xa4\x05\x14\xea\x19\x9e\xf4\x2c\x48\xa4\xf7\x3b\x17\xb8\xc5\x5f\xa4\x99\x73\x9f\xaf\x98\x55\xe5\x26\x56\x84\xc1\x5f\xbc\x42\xa1\xd9\xc0\x4f\x39\x63\x82\xc8\x30\xb7\x44\xa6\x76\x45\xc0\x8f\xa6\x49\x23\x20\x81\x3b\x85\x12\x24\x8d\x16\x23\x4b\xfe\x91\xa8\xd6\xab\x45\x54\x38\xc9\xad\xd8\x09\x4c\x1a\x83\x92\x4c\x26\xa8\x86\xbf\x44\xfe\x0d\x0b\xa5\x78\x09\xcc\x45\x16\x25\x62\x1d\x08\x9c\x87\x3f\x1d\x76\x08\x22\xa2\x27\xa1\x57\xf4\xcc\xaa\x80\x7b\x3b\xac\x6b\xa7\x54\xaa\xb2\xd7\x9d\x7a\xd7\xdc\xca\x35\x0f\x35\x35\xfe\x71\x03\x32\x48\x29\xc0\x8e\x9e\x94\xf6\x05\x0f\x68\x93\xd2\x44\xf1\x80\x3e\x04\x40\xfb\x22\xaa\xba\x9a\xe5\xdd\x2e\x58\x0e\xa2\x93\x9f\x3f\x64\x09\x0f\xa4\x0d\x5c\xa1\xd7\xbc\x8a\x25\x2b\xbe\x80\x01\xe2\x99\x42\x28\x8b\x96\x66\xfc\x7b\x62\x06\xf1\xe2\x46\xbc\xec\x99\x4a\x2c\xb3\x48\xc4\xb8\xe4\x83\x32\x56\x99\xe0\xde\x33\xca\x60\xb3\x7d\xda\x72\x12\x24\xaa\x13\x07\x42\x98\x16\xd7\x93\xaa\x9b\x33\xca\xe6\x9e\xac\x51\xd6\x61\x50\x47\x86\xc4\x08\xf3\xb6\x7c\x86\x44\xe9\xe4\x7c\x95\x0b\xa5\xd3\x4b\x2b\xe9\xd2\x1d\xf9\x5d\xe9\x22\x10\x99\xdf\x49\x17\x01\xae\xdb\x46\xc9\xd2\x0e\x10\x59\x06\x7e\x0f\x63\xa2\x71\x7c\x5e\x64\x73\xc7\x50\x6c\xeb\x7f\x47\x88\xbc\x98\x71\xc8\x2d\x18\x7b\x8d\x7b\x33\x80\x93\xbb\x85\xa7\x8a\x4c\x10\x48\xbc\x0c\x9e\xe0\xe1\x39\x07\x22\x07\xaa\xe3\xe9\x60\x76\xd0\xbe\x8a\x74\x97\xa5\x78\x30\xb8\x18\x5d\x2c\xa6\x72\x65\x49\x1e\xfe\xfd\xce\x1a\xef\x64\x6f\x2f\xdd\xca\xd6\xc6\xb0\x95\x24\x75\x3d\x8b\x92\x5e\x25\x1b\x2c\x1a\x30\x7f\xbc\x6a\x9d\xbd\x2a\x89\x00\xd7\xad\xa6\x4d\x2a\xf3\xa8\x5b\x46\xbb\xc1\xe4\xd4\xb3\xaa\x5b\x86\xb1\x41\xf2\xa6\xca\x7f\xd6\xe2\xcb\xf9\x8d\x59\xfc\xc6\x9a\xf8\xcd\x20\x97\x54\x5a\x45\xac\x36\x6f\xc1\x61\xb4\xc6\x2e\x53\xc3\xf8\x64\x88\x68\x4c\x6e\x66\xdb\xb9\x99\x6d\xe3\x66\xd6\xcc\xcd\x4c\x71\xb3\x40\xf0\xbf\x80\x05\xc3\xf6\x29\xbd\xa4\x78\x06\x96\xe3\xc1\x63\xd8\x1c\xd7\xde\x1e\x33\x38\x1e\x19\xbe\xdc\xb1\x70\x48\xa0\xca\xb4\xb7\x67\x39\x61\xe6\x11\xf3\xb1\x3a\x2c\x7f\xdd\x63\xca\xc5\x7c\xdd\x65\xea\xec\xfa\x29\xe8\x69\x3d\xaf\xfc\x14\x5d\x8a\xd6\x75\xb5\xa5\x83\xf1\xa4\x46\xb4\x47\xbb\x61\x4a\xf2\x1e\xfc\x93\xc0\x17\xce\x5b\x5b\xc0\x6c\x99\xe9\x1f\x22\x40\xe4\xf6\x8c\x1c\x5b\x24\xdc\xd8\x02\xa1\xc4\xdc\x44\xc9\x2b\xbf\xac\x12\x4a\xda\x54\xdb\x65\x88\x1f\x22\x3c\x84\x4d\xeb\xb1\xd1\x69\x9b\x86\xe5\xa0\x0c\x4b\x94\xe3\x60\x8e\x0e\xd4\x75\xa0\x06\xc5\x4e\x67\x12\xfe\x06\x3e\x0f\xca\xc1\x92\xca\xaf\x90\x1d\x0f\xe4\x0d\x65\x12\x4e\xe4\x8e\xdb\x77\x62\xe0\xcf\xb4\xa5\xae\x7f\x09\x03\x59\xfd\x14\x46\x8f\xec\xa7\xeb\x17\x31\x3b\x07\x46\x9e\x43\xad\x34\x93\x9c\x7c\x23\x13\xa3\x53\x3c\x84\x6e\xed\x7c\xa0\xed\x22\x0d\xb6\x90\x33\xa3\xb5\x7f\xc7\x61\xd0\x4e\x91\xfb\x78\xdd\x71\x47\xd4\xa2\x6c\x3c\xc9\xb2\x8a\x46\x95\xaf\x0c\x44\x3c\x51\xdc\x2d\x2b\x12\x46\x54\x97\x4d\x64\x97\xd2\xc1\xb8\xe6\x07\x9a\x59\xcd\xc0\xd0\x87\xb6\x14\x7b\x3d\x00\x3f\x18\x2a\x57\xad\x82\xc8\x84\x87\x58\x36\xb4\x97\x94\x10\xaa\xd5\x60\x89\xa7\x57\x6d\xe5\xc9\x2d\x71\x11\xe9\x73\x84\x42\xfc\xc8\xf2\x96\xe9\xc0\x69\xe4\x26\x96\x6f\x7c\x0b\x2d\x8f\x1f\x25\xb9\xcd\xaf\x58\xa1\x7c\x83\xa2\x62\xff\x76\x0e\x0c\x21\xfb\x3c\x68\xd8\xc2\xb4\x44\x65\x8f\x19\x73\xc1\xe4\x3a\xa3\xf3\x14\xa8\xc7\x6a\x59\x7e\xad\x74\xef\xa1\xa5\x6d\x25\x42\xcc\xbf\x89\x38\x0c\xc3\x1b\x23\xba\x32\x50\x1d\xd7\xad\xa3\xa9\xc9\x18\x57\x43\xbb\xbb\x37\xd4\x51\xc5\xce\xcd\x6c\x28\xeb\x6a\xe4\xc6\x1c\xbf\x6b\x73\xfc\x04\x07\x96\x7c\x23\xd7\x79\xd3\xd4\xaf\xd7\x62\xf6\x99\x60\xd0\x12\xc3\x2e\xe5\x37\x2f\xb9\xad\x16\xf1\x2d\xf0\x1a\xbc\x29\x1b\xab\x4c\xab\x9d\x29\x90\x85\xdf\x55\x71\xc0\x13\xd1\xfc\xc7\x1e\x0c\xdb\x17\x6c\xb1\x88\x69\x5b\xe2\xb1\xe7\x3c\x49\xbb\x7c\x96\x74\xf3\xc1\x58\x29\x7a\xa6\xff\xd5\xa9\xf1\xb9\x0c\x33\x2d\x22\x1e\x27\xd2\xb4\x27\x50\x15\xb7\x5c\x83\x15\x18\x03\x51\xc5\x4f\x29\xb1\x99\x1b\x60\xc2\x0b\x28\xf7\x22\x3a\xb6\x24\xf3\xea\x10\x18\xdb\x6a\x37\x00\x3a\xa6\x2a\xf2\xba\x78\x14\xaf\x89\x4c\x8c\xe3\xe0\x1d\x61\x03\x4c\x6c\x15\xc1\x21\x82\x11\x52\xca\x60\x50\x20\x7e\xf1\xb4\x36\xa1\xc9\x21\x06\xbd\x7e\xd7\x6e\xcb\x67\xdc\x0f\x7f\xcc\x48\x71\xe2\x4f\xca\x75\x2c\xa4\x55\x79\x2e\x63\x3c\x8c\x8e\x52\x7a\xc5\x4d\x2f\x3f\x18\x0a\xd9\x7c\x13\xe6\x4a\xe3\xcc\x4d\xf1\x2d\x0a\x68\xe9\x5b\xdf\x63\xec\x29\xf3\xfe\xda\xe5\xfd\x97\xd3\x43\x4e\x13\xe1\x65\x37\x16\x5d\xa5\xfb\x5a\xf9\xb8\xf9\x5a\xe0\x95\x35\x5f\x55\xd4\x00\x5b\xd9\xbd\xe9\x59\x2d\xea\xda\x6b\x14\xcc\x9c\x5e\x25\x24\x5c\x0a\x0e\x6b\x04\x07\xe3\x80\x75\x3d\x3d\xba\xaa\x73\x3b\xb4\x5b\x63\x09\x37\x0b\x34\x1b\x84\xdb\x97\x96\x97\x95\xd1\xd6\xcb\x85\xd5\x28\x52\x76\x95\xc9\xe8\x0e\xe9\x31\xa9\x99\x9d\xbb\x58\x7c\xb6\xd4\x34\x44\x0b\x17\x90\xf7\x08\x16\x53\x40\x59\xfb\x0d\xa4\x70\xcd\x87\xba\x38\xc2\x1c\x62\xae\x14\x5a\x03\xe1\xbf\x2c\x2e\xf0\x86\x7c\x2d\x2b\xf1\xf2\xe9\x61\x6e\xde\x89\x59\x52\x09\xbd\xdb\x2a\x4a\x35\x39\xcc\x49\x45\x6d\xbe\x39\xa8\xa2\xdd\x91\xef\x10\xd8\xce\x1d\x07\x09\xc6\xa3\xbd\x76\x6d\x35\xe2\xef\x55\x58\x91\x34\x89\x5b\xf8\xb5\x9d\x16\xf4\x05\xf1\xec\xdf\x84\x94\x8d\x72\x4b\xf2\x45\x46\x04\x0a\x3e\x6c\x75\x61\x05\x5a\x03\xa7\x6b\x16\x2f\x7e\x90\xd7\x55\x55\x61\x6e\xe4\x31\xcc\xe3\x13\x7d\xd8\xb9\x08\x2f\x45\xa3\xf4\xc1\x5c\xdc\xa4\x1c\x4e\x94\x4a\xcf\xcf\xed\x4e\x58\xb7\xeb\x9b\x18\xc4\xde\xe9\xa1\x5a\x29\xcd\x33\x9a\xea\x1c\xa6\xc8\xbb\x60\x49\x97\x75\x2a\x15\xf4\x8b\xf4\x15\xbb\xa6\x0b\x7e\xfb\x88\x14\xc7\x02\xb1\x61\x50\x49\x17\x91\xdc\x5b\xcf\xf5\x1d\x5c\xb3\x66\xd1\x2f\x89\xf3\x83\xe1\x86\xc0\xd0\x7c\x2f\x6e\xe9\xaa\x45\x3f\x5d\x9b\x6d\x50\x5e\x59\x4e\x74\x7e\x08\xac\x57\x74\xbb\xc4\xec\x51\x11\x92\x51\x45\x56\xc1\x25\x2e\x8c\xa9\xe3\xea\xf5\x2c\x5c\xf0\xa7\x86\x82\x5b\xfe\xc2\x97\x67\x36\x7c\x66\xf1\x47\x30\xd4\x4e\x83\x26\x50\xb9\xc2\xd4\xc4\x73\x6f\x3f\xa8\x49\xc3\xb2\x4e\xb9\x9a\xac\x4c\x56\x56\x95\xf4\xac\xbc\x89\x0c\x10\x08\x6d\x98\x0a\x3e\xce\x58\x52\x9b\x93\x07\xfa\x7f\xe0\x46\xa7\xc7\x64\xef\x98\x1c\xaa\x7a\x4c\xc3\x8a\x6e\x7e\x97\x16\x91\xdc\xc1\x63\xd3\xed\xd5\xf9\xf7\xd0\x43\x64\x57\x19\xfc\x45\x1e\x44\xde\x41\xb5\xb8\xe4\xc8\x46\x92\x37\xa4\x21\xab\x1e\xd8\x64\xe9\x42\x67\x5a\x0d\xc2\x20\x8c\x2d\xaa\x90\xe4\x39\x0c\xee\x28\x03\x85\xef\x85\xb6\xd8\x55\x69\x73\x0a\xc3\x44\xad\xc3\xd7\x5c\xf2\x48\xc1\xf6\x4e\x04\xda\x81\xba\x20\xc5\xb9\x95\xfb\x1a\x56\xbd\x50\xdb\xaf\x46\x07\x75\x47\xc3\x19\xea\x0a\x8e\x74\x62\x50\xa1\xba\x25\x94\xa9\x96\x4f\x38\x21\xe9\x16\x1d\x4f\x36\x43\x8a\x8e\x6b\x43\x74\xa4\x5a\xb0\x59\xf7\x43\x54\x46\xcc\x53\xc2\xd9\x48\x82\x25\xb3\xf7\xbc\x86\x70\x1a\xed\xed\x71\x85\xa8\x4e\xb7\x8c\xbf\xa9\x00\x3b\x00\x0f\xc2\xff\x1e\x3a\x33\xa6\xc3\x89\x2f\x6f\x0c\x41\x57\xe6\x91\xe7\x80\xc1\x05\x33\x4c\x3a\x14\xe6\x5d\xd2\x01\x4b\xa8\xaa\x6a\xd5\x7b\xff\x73\xed\x1f\xa8\x8d\x56\xad\xb4\xef\x18\xd7\x77\xa5\x75\x50\x5d\xbb\x6c\xd2\x94\x9c\x34\xfb\x3f\xe9\xa4\x13\x67\x4b\x15\x51\xa5\x6c\xd1\xbe\xda\x86\x76\x77\xd2\xee\x67\x72\xc3\xbf\x56\xcd\x70\x3b\xdd\x96\x34\x73\x74\x83\x01\x63\xcc\x56\x21\x08\x82\x39\x57\x52\x21\xeb\x05\x58\xb0\x4d\x6e\xb5\x52\xc0\xf4\xac\x66\x99\xe8\x9a\x74\x71\x03\xbd\xe7\x0f\xf4\x3a\x29\x10\x5d\x44\xd7\xd6\x66\x75\xd9\x61\xac\xa3\xb6\x5f\x00\xd8\x92\x32\x8e\x8a\x5c\x5d\x3a\x1d\x1a\x1a\x72\xaf\x81\x47\xba\x0d\x1d\xeb\x63\x24\x13\x1b\xa8\x6b\x90\xbc\xda\x42\x27\x1d\xfb\xdc\x28\xf9\x6b\xc6\x16\xa8\x0c\xe1\xfd\x27\x23\x1f\x6f\x78\xcb\x43\xda\x71\xe1\xc6\x2a\xad\xe6\x56\x10\x00\xdb\xe4\xdd\x90\x73\xaa\x11\xa8\x81\xd1\xa2\x8e\x9d\x43\xb5\xe5\x6d\xef\xb7\xca\xa5\xf7\x5e\x9f\xb4\xb7\x75\x1d\xf1\x07\xa5\x3a\x04\x3d\x61\x10\xe3\x6e\x94\x94\x4d\x5a\x8b\xa9\x3b\xd7\x0a\xa7\x05\xc0\x85\x5c\xe2\x99\xfa\x4c\xa9\xa5\x1b\x07\xcd\xaa\xbc\x91\x92\xa5\x49\x54\xe4\x93\x38\xd4\x9a\xd3\x57\x69\xc6\x7e\x01\xc4\x51\xcc\x3b\x75\x62\x59\x2f\xc2\x3b\xd3\x6c\xbe\xd8\xea\x17\xfa\x9e\xb5\x29\x90\x10\xda\x03\xa1\x0e\x95\x0d\x9f\x84\x61\x7a\x77\x17\xdf\xdd\x79\x31\x46\x6c\x92\x18\x01\x4d\x3f\x58\x3a\x1d\x62\x28\x6d\xc5\x6d\x7e\x2e\x07\x5a\x39\xce\x6b\x4e\x77\x05\xc0\x7b\x8a\x47\xd2\x56\x30\xc4\xdb\x4a\xc7\xba\x24\x59\x76\x43\xf3\xce\x27\x13\x11\xa7\xd6\x33\xbc\xf6\xcb\xaa\x83\x4d\x2c\xb3\x4b\x73\xdf\xcf\x36\x08\xfd\xfa\x8e\xc0\x6e\x84\x35\x79\x0b\x41\x6d\xb3\xc8\xa0\xdb\x6a\x57\xdb\xd1\x89\xb5\xe4\x57\xac\x3b\xc3\xb1\x61\x2c\x7b\xd4\xef\xca\xc9\x6c\x74\x47\x52\x07\x6b\x98\xdf\xfd\xfd\x00\x16\x4e\x07\x8a\xd4\xb5\x64\x82\x04\x8a\x4a\xae\xfc\x3b\xcd\xf0\x86\x54\xc9\x93\xc8\x41\xf4\xee\x2e\x02\x0e\x8a\x38\x07\x45\x55\x0e\xa2\xbf\x33\x07\x45\x26\x93\x24\xb6\xaa\x6c\x8c\x54\x52\x55\xb1\x7b\x9f\xfd\x86\x7c\x73\x2f\x5d\x2a\xa7\xbb\xdf\x4c\x15\x66\xf0\xf8\x13\x94\x0a\x18\x47\xc1\xe3\x6a\x73\x92\xce\x6c\x14\x23\xa9\xe4\xe9\x84\xcf\xb1\x44\x86\xa3\x4d\xbd\xde\xa8\xd3\xa8\xd1\x54\x05\x9e\x29\x90\xd2\x99\x72\x18\x37\xf9\x6c\x01\x42\x4a\xa7\xa0\x5d\xa4\xab\xb6\xed\x9d\x18\x92\x21\xa7\x42\x44\xc2\xe8\xc9\xa1\xb6\x2d\xf1\x1e\xbb\xf8\x11\xf6\x35\x9e\xa4\x09\x2f\xbc\xe3\x9a\x2d\xc5\x65\xc3\x89\x4f\xaa\x6b\xbb\xc3\x44\xcb\xb9\x2e\xd5\xab\x7a\x50\xc5\xba\x78\x1a\xcd\x7f\x5e\x64\xe9\x4a\x2e\x22\x3f\xf8\x81\x51\xc6\x58\xee\x0f\x79\xa7\xc8\xf8\xdc\xfb\x55\x0c\xa5\x60\x44\xd7\xa5\xf2\xa0\x6f\x19\x28\x93\x3a\xac\xbe\xa4\xca\x96\x63\x9f\xbf\xc8\xe6\xb3\x5f\xb5\x81\xf2\x9b\x16\x7c\x68\x8e\x8c\x43\x57\xb6\x41\xf6\xaf\xe8\xe1\xe0\x3e\x5a\xbc\x8c\x65\x7d\x71\x46\x33\x6a\x5d\x4c\xa0\xd5\x4a\x1d\xca\x32\xa8\xaa\x56\x1b\x62\x76\x4b\x4d\x87\x29\x8f\x09\x12\x3c\x66\x10\x93\x8c\xcc\xc9\x9a\x2c\x2a\xcc\x0a\xc3\x22\xf4\x5e\x9c\xa8\xbc\x8d\xaf\xb4\x83\xc0\x60\xe4\xc1\xf8\x04\x6f\xae\x33\x56\xbd\x73\xe8\xab\xd2\xa8\x57\x26\xab\xdc\x88\xa8\x63\x23\xae\xf4\x72\x4b\xc2\xce\x50\xfb\x12\x62\x3c\xaa\x0d\x17\x83\x22\xa5\x38\x15\xc2\x54\x07\xf7\x32\xb2\xf0\x9b\xcc\xe4\xad\xc3\x26\x78\x5a\x18\xc5\xec\x04\x86\x47\x6e\xec\xee\x13\x34\xf4\xd8\xdd\x1d\x53\xf6\x9e\x41\xca\x60\x8c\x17\x9f\x70\x43\xa8\x7f\xdd\xcd\xa7\x2b\x58\x0b\xa0\x97\xf0\x1b\x56\x4b\xbe\x99\x77\xdd\xcb\x0f\xce\x21\xf9\x3c\xe4\xdf\xb0\xd2\x31\xdf\x0f\xea\xad\x42\x47\x3f\xee\xfc\x1a\x38\x28\xc7\x11\x48\x5f\x87\x05\x8b\x8b\xe0\x75\x8f\x1a\x98\x29\xc7\x3c\x89\xc3\x73\x92\x19\x57\x12\xae\x8c\x6d\x7b\xbd\xfc\x42\xaf\x7d\x8d\x37\x0e\xf3\xc0\x12\x2f\xc1\xdb\x11\x1d\xe9\x11\x5e\x11\x9b\x0d\xb4\x41\x96\x76\xcb\x0b\xa0\xc8\x3a\x8c\xcb\x9c\xa5\x99\x33\x0f\xcf\xbc\xb9\x3f\x9b\x07\x43\x00\x3a\xf3\xd6\xfe\x6c\xad\xdc\x54\xa6\x94\x58\xf4\xbc\x75\x77\xee\x2b\x1b\x12\x78\x59\x48\x30\x11\xf1\xb4\x26\x73\x9f\x33\xb8\x91\xd8\x1c\xae\x26\x4c\x17\x19\x70\x66\x54\x82\x1e\x90\x6e\x35\x51\x1a\xa2\x02\x71\x08\x76\x04\xd5\x44\xdc\xc8\x44\x6b\x7e\xf0\x69\x5a\x76\x8b\x43\xb6\x36\xcf\x51\x6d\xc3\x74\x58\x4f\x77\x11\x47\x68\x71\x6d\x73\xcb\xec\x11\x29\xfc\x32\xe2\x49\xfb\x07\xa8\x8e\x43\x94\x6d\x52\xd1\xeb\x38\x32\x65\xa6\x6c\x5b\x2d\xfa\xe9\xfe\xe8\x3b\xbc\x0c\xb2\xc1\xf2\x50\xf7\xd3\x82\x0e\x26\x35\x49\x8c\x4d\xeb\x78\xb5\xd5\xa6\x14\xa9\x9a\x09\x25\x41\x32\xc6\xc0\x52\x71\xe4\x61\xaa\xed\x7a\x4e\xb3\x7e\x64\xac\x4e\x7e\x73\xe8\xa0\xec\x2c\x93\x16\x90\x96\x3b\x04\x0f\x7a\x72\xfb\xc9\x42\x6d\x38\x98\x86\x93\xa8\x2e\xb3\x22\x90\x59\x79\x83\xcc\x8a\x2a\xae\x47\x73\x29\x2f\x57\x13\x5a\x5d\x4d\x7c\x2e\x99\xa2\x99\x56\xf2\x70\x07\x0c\xa3\xa0\x82\x32\x6e\x48\xa6\x4c\x9a\x5a\xb2\x51\x7d\x5f\x9a\x72\x82\x21\x3e\x60\x5b\x5a\xa3\x7a\x29\x57\x76\xc1\x1a\xcb\xa6\x23\xf0\x35\x13\x58\x69\x04\xe6\x1e\x93\xb1\xcd\x22\xf9\x66\xa9\x96\xae\x8a\x02\xf1\x3d\x49\xab\x5b\x2c\x4e\xb8\x1f\x88\x8e\xda\xa9\x61\xb0\xca\x37\x40\xfd\xe0\x6f\x1e\x11\x49\x60\xee\x26\x96\x07\x87\xea\xdb\x68\x92\x21\x53\x7e\xb9\xae\x30\xe3\x9e\x58\x8c\x7d\x5b\x9b\x01\x3c\xec\x6e\xab\xcd\xa3\x21\x78\xb5\x9a\x65\x59\x6d\x5d\xeb\x8d\x26\x0c\xaf\x54\x67\x78\xa5\xba\x3e\x44\x69\xe1\x57\x33\x9e\x36\x2e\xc4\xf7\x32\x75\x74\x8d\xdb\x3c\xdb\xc2\x62\x1c\x73\xd4\x34\x7e\x81\xbb\x29\x46\xfd\x35\x1a\xc0\x86\x71\xfb\x48\x02\xc1\x1e\xaa\x84\x9d\x7d\x24\xdd\xa6\xc6\x0e\x36\x98\x18\x21\x23\x32\xcb\x0a\xb8\x50\xf6\xb2\x95\x08\xab\xd8\x32\x8c\xe0\xdf\x38\x5c\x4e\x51\x61\x99\xa6\x3d\x3c\x5f\x8a\x8a\xcb\x52\xe8\x2f\x90\x60\x79\x79\xb8\x6a\x33\x53\xac\x1b\x30\x2e\x4f\xf4\xcf\x68\x0a\x79\x22\xcc\xa7\xc1\x1d\x94\x95\x26\x57\x3c\x6b\x9f\xa6\x45\x91\x5e\x38\xac\x2f\x5b\xb7\x22\x4a\x2a\x6d\x36\x7c\x5f\xb3\xbc\xa2\x47\x5e\x0d\x42\xda\xe2\x86\x82\x36\xa9\x2a\xb5\x8e\x2b\xe8\xed\x4b\x9a\xd0\x2d\x6e\x5c\x56\x65\xc0\xc1\x5a\x58\x1e\x91\xb6\xcf\x3b\xe8\xc3\x42\xd6\x3d\x0f\xea\x92\x07\xfc\xc0\x83\xfe\x18\x80\x8b\xc7\x38\xf7\xc5\x15\x50\xa0\x0e\xcf\xc4\x09\x6a\xaf\xae\xb5\xd3\x8d\x1f\xe0\x5d\xc3\x20\xb4\xca\xe3\xd5\x17\xe9\x62\x8d\x11\xda\xe2\x2f\x18\x97\xab\x34\xe3\x27\xac\xed\x84\x90\x6f\x40\xf3\x37\xad\x42\xbc\xf1\x3f\x49\x0f\xd3\xe4\x2c\x66\xf3\xc2\x71\x2c\x48\x01\xc2\x42\x0c\x5d\x69\x9c\xc1\x37\xbb\x6e\x97\xb7\xb3\x98\x7a\xee\x04\x74\xa6\xdb\xfa\xfb\x44\xc3\xf2\xcd\x23\xed\xe5\xd1\xa9\x7f\x35\x5d\x29\xf6\xf3\x49\xc3\x7d\xf5\x7e\xd2\x5f\x4d\x87\x8c\xf9\x86\x52\xc5\x0b\x69\xd5\x65\xf9\x82\x30\xe7\x34\xca\x8e\x74\xc0\xab\x4a\x28\x63\x6c\x83\x31\x26\xf0\xe5\xf1\x68\x15\xcd\xf1\x24\xde\x3e\xa6\x7c\x21\x0e\x25\xab\xb4\x11\xb1\xaf\x1a\x08\x9e\x1d\xac\xe3\xd6\x3c\x8e\xf2\x3c\xc4\xc7\x67\xf0\x70\x12\xd8\x6a\xaf\xd3\x2b\x9a\x1d\x42\x41\xcf\x7f\x3a\xed\x89\x22\xed\xe9\xc1\x53\x7e\x12\x48\x4a\xce\xe1\xa4\xc5\x0e\x2a\x87\xb7\x21\x09\x1f\xbd\x78\x3a\x3d\x88\xd9\xf4\x20\x5f\x45\x49\x8b\x1f\xe3\x0f\xdb\xa7\x3a\x90\xa1\x37\xe7\x9d\x05\x95\xe9\x93\xda\xec\xa4\x0c\x30\x79\x3a\x85\x8a\x06\x58\x74\x8a\x0f\xea\x78\x26\x90\xf9\xb6\x4e\x2d\x1d\x53\xf1\x95\x9d\x01\x56\x2d\xbe\xd6\xf1\xf4\xd9\x66\xc2\xcc\xf3\x5e\xde\x2d\x36\x31\x68\xbf\x8c\x60\xc1\x52\x87\xa3\x02\xe0\x08\x87\xa3\xa3\xb0\x7c\x84\x72\xaa\x08\x25\x9a\x5b\x9f\x87\xbc\xdb\x98\xf8\xa1\xf1\x57\x3c\x82\xfc\xa1\x28\x25\x69\xa1\xd6\xef\x83\xea\x6d\x3a\xf2\x9c\x99\x25\x92\x51\xce\xf0\x71\xf5\xb4\xd6\x68\xf9\x28\x7b\x49\x19\xac\x6a\x62\x17\x65\xb4\xb6\xdc\x4a\xbb\x11\xa8\xc1\xac\x83\x6b\x78\x85\x17\xba\x91\xe5\x45\xd1\x35\x36\x9e\xfe\xbb\x67\xb7\xb0\x37\x16\xb5\x98\x3c\x68\xe1\xcf\x2a\xe8\x2b\xa7\x0f\x1c\x4d\xef\xf1\xbb\x65\x5d\xc4\xeb\x33\x66\x83\x02\x05\xa9\xd0\xba\x25\x47\xe0\x19\x7d\x73\xc4\xfa\xe6\xbb\x5d\x78\xde\xa3\xbc\x7c\xcb\x38\xd1\x55\x39\x6e\x29\xf2\x1d\xa7\x6a\xcb\xe7\xba\xf0\x96\x06\x7e\x46\x6f\xa6\x96\x59\x68\x63\xfe\xa2\x10\x02\xbd\xf0\x83\xe3\x13\xc1\x2c\x18\x65\x84\x59\x9e\x2d\x7b\x95\x43\xee\xb8\xad\x99\xbf\x4d\xda\x46\x50\x7d\x9b\x1f\xfe\xa7\x22\x4a\x89\x55\x24\xb7\x2e\x03\x44\x2c\x61\x89\x8a\x71\x99\x7a\x25\xa2\x73\x0c\x1c\x66\xae\x10\x17\x1b\x43\x59\x55\x07\x81\xf9\xfd\xe4\x3c\x15\xe8\x54\x6c\xad\x0f\x34\x68\xd6\x36\x62\xfa\xed\xb3\xa6\xb6\x2c\x22\x65\x64\x7e\x1d\x4c\x67\x39\x4e\x3e\x97\xad\x2d\xdc\xb7\x29\xc8\xbb\xd5\xf9\x7c\x0f\x98\x98\xf7\x77\x77\xfc\x29\xa2\xf2\x68\x31\xab\xdd\x53\xa0\xd2\xcd\x03\x0b\x78\x15\x03\x8c\xd0\x66\x62\xf1\x8e\xbc\x3b\xa1\xec\x75\x9e\x63\xbe\xbf\x94\xe0\xb6\x1a\x16\x16\xa0\x78\x8a\xd2\xea\x38\x4f\x5d\xbb\x2e\x0e\x94\xa3\x45\x22\x96\xff\xe4\x84\xc8\x5a\x5e\x9b\xe4\x6f\x21\xd1\xdd\x24\x6b\xb0\x21\xc7\xfa\x8d\xe7\x54\x1d\xa0\x72\x30\x58\x95\x17\xf8\x5d\x14\xe6\x5d\x0d\x3a\xec\xca\x38\xcf\x27\x1c\x8e\xf2\x95\x43\x15\x4e\x6a\x73\x8b\x71\xbc\x15\x8f\x3c\x48\x3f\xa2\x10\x8b\xd2\x15\x4e\x9a\xa6\x82\xb8\xac\x4a\x33\x59\x41\x6e\x8d\xe0\x4e\x81\xc3\x21\xe1\xac\x41\x93\x4f\x23\x91\xeb\xa6\x42\xdf\x3b\x0b\x10\x10\xfb\xbe\x3a\x2b\x65\x53\xbb\x29\x5d\xfe\x56\x57\x08\x85\xc8\x73\x6c\xb7\x2a\xaf\x38\x47\x23\x32\xbd\x92\x9d\xb9\x15\x61\x5d\xc5\x42\x1e\x25\x0c\xb6\xc9\x13\x49\x6e\x8d\x52\x05\x6e\x49\x5e\xba\xfd\xe2\x12\x39\x24\x62\x0a\x20\xb7\x13\x31\x80\x24\x2f\x39\xc4\x16\x78\x16\x76\x6d\x5f\x11\x2e\x8d\xf1\xd4\x9e\xe3\x1e\x4e\x54\x5a\xed\x7b\x74\x54\x0c\x1a\xaf\x92\xdf\x97\x48\x0c\xaf\x42\x55\xd9\x40\xcf\x02\xd6\x64\x38\x4b\xb5\x56\x10\x89\x3b\x61\x14\x28\x93\x8f\x88\xb9\x60\x50\x83\x90\x07\x81\xbc\x04\x14\xf4\x04\x14\x74\xfd\x9e\x90\xab\x9b\x52\xd0\xdc\xf5\x05\x30\xd2\xff\x2e\x1e\xe7\xdc\xe5\x58\xbb\x50\x43\xcb\x2b\x64\x98\x63\x3c\xf9\xad\x93\xbc\x2b\x98\x3a\xf4\xca\x1f\x75\x4b\xc2\x5b\xdb\x87\x6b\xcb\xd7\xf2\x29\x4e\x75\x08\xca\x90\xaf\x72\x8b\xc6\x98\x5c\xe6\x35\x50\x8e\x6b\x28\x1a\x2e\x61\xb0\x1f\xd0\x6c\xb8\x30\xc1\x7a\x8e\xb3\xe9\xf2\x05\xfb\x89\xce\xc6\xcb\x1c\x2a\xef\x83\x12\xc3\x5a\x0f\xd4\x25\x44\xe4\xd4\xd0\xd9\xeb\xa5\x0d\x8d\x1e\x34\x3d\xe3\x49\xd2\x3a\xa8\xf9\x62\x69\x2d\x14\xa6\x3e\xbe\xd4\xf5\x4e\x8f\xc7\x03\xe5\x5c\xe1\xad\x06\x99\xea\x14\x64\x59\x9d\x3f\xa1\x56\xc0\x06\x5e\xb9\x2d\xb7\xae\x83\x82\x77\x4f\x40\x8d\xeb\x1c\xbd\x2d\x3d\xbe\xad\x9f\xb7\x75\xae\x4f\xb4\x6b\xc5\xc5\x5a\xda\xc5\xa2\xdd\x91\x0d\x50\x72\xf5\x75\x18\x39\x75\x78\x87\x29\x44\x6a\xdb\xe1\x0d\xe5\x2c\x18\x62\x6d\x7d\x37\x94\x28\xb7\xea\xeb\xf9\xb6\xa5\x36\x24\xd6\x4e\xf9\x07\xe0\x73\xbc\xe5\x0a\xc6\xe1\xca\x75\x63\x8a\x7e\xe8\x77\x36\xdc\xa2\x3b\xcd\xb6\x68\x5f\x81\x78\xa8\x56\xf1\x8d\x93\x68\xe9\xcd\x56\x07\xe3\xdd\x14\x48\xd5\xc8\x2a\xac\x5e\x1d\x46\x35\x5a\xb2\x6a\x42\x6e\xab\xd3\x84\xf2\xd7\xf5\x88\x78\xe2\xc8\x41\x01\x8f\x19\x2a\x1f\x3e\x72\x43\x08\x46\xc3\x27\x91\x5c\xf9\xea\x1d\x63\xfe\x5a\xd2\x36\x80\x6e\x43\xfd\x9d\x2d\xb5\x6a\x15\x99\xef\x8a\x6b\x15\xaf\x34\xfa\x3c\x8c\x37\x83\xe1\x43\xa3\xa4\xb2\x85\x41\xeb\x4f\x4c\xaa\xbb\x14\xca\x95\x87\x9e\xec\xa2\x42\x16\x52\x85\x64\x0f\x56\x6c\xa8\x41\xbf\x15\xc9\x3d\x6a\xd0\x77\xc8\xa3\x75\x2e\xd4\xf7\x9c\x18\x4d\xbd\xb6\xd6\x7a\xb7\x9a\x5b\x03\xd3\x59\xa0\x99\xfe\xc9\x52\xc4\x44\x65\x3a\xa8\x5d\x47\x2b\x2b\xcd\x4b\x85\xa9\xdb\x23\x64\x29\x69\x66\x1c\x7b\x45\x55\xab\x6b\x44\x5c\xc3\xe2\x63\x26\x23\xdb\x2d\x6a\xcc\x5a\xf9\x1d\x99\xd6\x3d\xd1\x86\x4c\x77\x68\xcc\xe4\xd6\x3d\x22\x4d\x1d\xbb\x51\xa7\x75\x4a\xbb\xec\x81\x8b\xfd\x46\xc6\xe2\x59\xba\x6a\x51\x0b\x09\x2c\x75\x98\xbb\xbb\x91\x28\x21\x6f\x9e\x9c\xd8\x9a\x82\x89\x8d\x5f\x32\xc4\xee\xed\x4d\x43\xbf\x14\xdd\x5a\x9d\x2e\xd6\x75\x78\x1c\xa6\xc9\xac\xe8\x97\x17\x9b\x78\xb7\x0f\xb7\x00\x68\x65\x46\x94\xb2\x0c\x6a\x15\xba\xd7\xa3\x27\xc7\x06\x04\x82\xbc\x76\xa9\x62\x58\x6d\x36\xe6\xcd\x8d\x1f\xe8\xb3\x14\x97\x09\xda\xde\x41\x95\x68\x4c\x42\xf9\x66\xb4\x95\xa3\x7c\x87\x80\x0b\x7d\xdf\xd1\x39\xe8\x3f\x09\xcd\x0e\xd7\x45\xba\x2e\x82\xfd\xda\x4b\xf5\xa3\x61\xf3\x53\xf5\xe2\x85\x88\xb6\xcc\xa7\x3c\xbc\x89\x96\xaf\xdd\x53\xf9\x7e\xfd\xc7\x76\x41\xaa\xbb\x14\x1f\xe5\x82\x54\x85\xb7\xba\x20\x4d\x20\xd3\x05\x59\x4b\x7f\x98\x0b\xf2\x8b\x74\x7d\xbe\x4c\xd6\xc5\x2e\x7e\x48\xc1\x7b\xb2\xbe\xd2\xc1\x55\x5e\xc3\x11\x7a\x14\xf7\x4a\x65\x84\x8b\x75\xc1\x6b\x55\x2a\x9c\x48\xef\x9d\x5e\xfb\x6a\x0c\x31\x18\xeb\xc0\x80\x23\x91\x89\xb7\xb9\x30\xbc\xd3\x45\xb7\xc1\x61\x30\x5c\xd7\x24\x0d\x60\xb9\xa9\x8b\xa5\xc1\x78\xe3\xff\x4e\x1e\x3a\xd9\x9c\x8a\x97\xce\x14\x56\xfa\x6a\xce\xfb\xed\xf3\x07\xbb\xe7\xe4\x52\xb6\xcd\xff\xa6\x25\xcb\x7b\x98\x3e\xb1\x57\x94\x92\xd4\x75\xf9\x93\x54\x41\xb8\x13\xc5\xb8\x16\xb6\xe2\x00\xa8\xb7\xdb\x21\xe4\x81\xa7\xf2\x46\x33\x7d\xe2\x30\x87\x1b\x3a\x4a\x59\xd2\x6a\xab\x0b\x2f\x4a\x51\x2f\x6b\x4b\xf2\x98\x5b\x77\x2a\x63\x72\x99\x3c\x4a\x52\x9d\xda\x76\x22\xbe\xec\x38\xc7\x28\xcc\x21\x29\x55\x34\xcd\xae\x5a\x89\x12\x42\x9c\x18\x73\xc5\xd6\x17\x4d\xe9\xa4\x95\x6e\xe3\x7e\x1b\xe3\x9e\x9a\x07\x15\x1c\x80\xac\xb4\x95\x4c\xa7\x90\x35\xd5\x9f\xfe\xdc\x76\xdb\xa9\x64\x9d\x80\x77\xab\xce\xb5\x69\xe5\x72\xb1\xd6\x16\x03\xd2\xe8\xb4\xad\xcf\xfb\xba\xce\xe6\x84\x55\xae\x55\x75\x41\x4f\x30\x52\x8f\xc2\xbd\xfd\x9a\xcc\xad\xb0\x3e\x57\x87\x89\x85\x41\xf5\x58\xb9\x0b\x6d\x16\x2c\xd7\x5e\xcb\x8b\xba\xc1\x53\x17\xf8\x5e\x87\x60\x70\xf3\x8e\x71\xa5\x82\x99\x07\x41\x1a\x42\x0c\x8b\x7a\x8c\xa1\x7c\xdc\x0d\x9f\xf4\x52\x27\x1b\x0a\x9c\x80\x26\x36\x3e\x23\xeb\x52\x99\xc3\x85\x43\xd7\x3c\x35\x21\xba\xe5\x23\xba\xaa\x6d\x5a\x25\x68\xf0\x25\x56\x65\x81\xc9\xfd\x1f\xe8\x5a\x34\x45\xd7\xb6\xd9\xdc\xe4\x53\x74\x29\xd7\x86\x4f\x44\x3d\x3b\xce\x9f\xd4\x0e\x5c\x73\xb9\x37\x72\xcf\x66\x86\x87\x63\xb6\x0c\xef\xfd\x1a\x76\x29\x02\x4c\x1d\xfb\x11\x6b\xd3\xaf\xba\xb8\xee\xd0\xf7\xb2\xc9\xb7\x75\xc9\xb5\x55\x3a\x3d\x58\x02\x6d\x4a\x46\x6c\x32\x00\x60\x10\x6d\x0b\xe0\x3e\xfa\x85\x0e\x6f\x6a\xe6\x0e\xb9\x70\xdf\xd4\xff\xd5\x1b\x4e\x78\x9c\xaa\xba\x77\x2c\x2c\xcc\xbb\xdb\x40\xd0\x9a\x54\xe1\xd5\x9c\xe2\xd2\x74\x8c\x1c\xa1\xdc\x26\x29\xa1\x43\x43\x02\x42\xdf\x1c\xb8\x39\x5e\x1d\xf9\xd3\x7a\x23\xed\x8e\x4e\x4c\x2c\x25\x2d\x86\x9d\x40\x98\xb8\x08\xb4\xaf\xf4\xc5\x8a\x1a\xf9\x96\x51\x43\x83\x04\x46\xa5\x19\x2e\xbf\xe4\xd6\xad\xd3\x0f\xf1\xe9\xa0\x8f\x67\x7b\xfc\x31\x62\x23\xe8\x2f\x0c\x1b\x98\x5d\xd2\xca\xcf\xf7\x14\x18\x30\x4d\x82\xfe\x9f\x09\x8f\x32\xfa\x22\x2d\x10\x42\x7d\x4b\xfe\x29\x33\x4d\xab\x68\x24\x52\xbf\xaa\xdd\x08\x19\x8c\x87\x6a\x2b\xaf\x34\xb8\xac\x04\x65\x56\xc9\x44\xbe\x53\x07\x30\x7f\xd0\xd8\x0b\x63\xd5\xff\x8d\xa2\x2f\x70\xe4\x76\x36\x7b\xb8\x9d\xaf\x36\xa5\xf9\x8f\xfb\x37\xa4\x1d\xe3\x49\x32\x87\x0e\x67\x33\x82\xdb\x21\xaa\x60\x48\xfd\x6e\x50\x07\x60\x9d\x5d\x5c\x7b\x2b\xf5\x8b\xe7\xca\xb7\x6e\x1c\x77\x8b\x96\xd7\x88\x8a\xa5\xd1\xb8\x23\xaa\x4e\x14\x5e\x34\xfa\xc7\x89\x89\xe0\x03\xd6\x14\x15\x21\x32\xff\xe0\x71\x11\xbf\x59\x48\x02\xe7\x1f\x95\x51\xfe\x10\xe9\x47\x56\xb1\x6a\x92\x80\x79\x7c\x40\x83\x28\x5e\xf1\x47\x97\x13\xef\x03\x83\x1a\x6a\xd4\x9a\x3d\x63\x34\xb4\x1a\xda\x20\x27\x54\x25\xbe\xc1\x55\x40\x07\x38\xd8\x45\xca\x28\x87\x2a\x09\x65\xa8\xc3\x7d\x31\x0e\x4e\x3e\x95\x9e\x7e\x1d\xb2\xe0\xf6\x3b\x7e\xef\xed\x1e\x5f\xf0\xc7\x0c\x33\x70\x4e\xd0\x6d\x81\x06\xa2\xc0\x43\x42\x0d\x8c\x40\x03\xc1\x85\x72\xe7\xd4\x0a\x34\xb0\xa4\xc8\xc7\xf0\x5e\x38\x3d\xfc\x7a\xa6\x4b\x42\x76\xf7\x6c\x98\xbe\x8d\x2d\xf1\x00\x22\x94\x90\x24\xdb\xe2\x01\xdc\x1d\xdf\x14\x11\x90\x7e\x8a\x08\xf8\x97\x88\x08\x48\x3e\x45\x04\x7c\x8a\x08\xf8\xb8\x11\x01\xb6\xee\xda\xbd\x57\xff\xfd\x4d\xa3\x02\xd2\x7f\x87\xa8\x00\x19\xfa\x8d\xef\x4d\x7f\x60\x34\xc0\x8e\xfa\xd7\x7d\x11\x01\x72\x97\xf3\x01\xfb\xfe\x5b\x77\xe9\xb7\xe8\x6d\x6e\x58\xa7\x6a\xf5\xbb\x6c\xd5\xcb\x0e\x7d\xe8\x66\xfd\xef\xbe\xb3\x6e\x5e\x4a\xa2\xd7\x5d\xfb\xc5\x72\xf5\x26\x01\x98\x18\xdc\xdc\xe2\xe3\xbc\xb1\x22\x29\x45\x4c\xad\x56\x7b\xce\xc0\xb8\x7b\x03\x4d\xfa\x6e\x49\x33\x74\xb1\x26\x90\x7b\x77\x57\x60\x70\xe5\xd6\x32\x6f\x33\x7a\xc9\xc0\xb4\xab\x96\x7b\xc4\x86\xbf\x3a\x09\x49\xfb\x57\x1c\x99\xd6\xb1\x12\x7f\xd2\xac\x76\xd5\x43\x01\x6c\xa7\xe2\xbd\x3b\xf7\x4d\xd3\x84\xf2\xbd\x79\x8b\x2d\x74\x34\x55\xe9\x3d\xe2\xf2\x0c\x49\x5b\x92\xfa\x39\xe5\x3c\x64\x78\x8b\x17\x3b\x58\x6a\x07\xdf\xac\x09\x91\x72\x43\x0d\xf1\x38\x2a\x2c\x6a\x59\x1a\x0b\x05\x0f\xfa\x03\x1d\xdf\xb0\x20\x20\x98\x87\x3b\x79\x4b\xee\x95\x24\xa9\xfa\xe4\x77\x3b\x5b\x85\x84\xab\xb3\x7f\x33\x75\x4c\xe8\x59\x03\xac\x2b\x76\x22\x68\x80\x3d\x30\x60\xcb\x5b\x5b\xb8\xcb\xf3\x5e\xdc\xc6\x05\x51\x35\xb2\xb9\xcb\x76\x47\xb2\x25\xec\x4e\x64\x4b\xd8\x9d\xc9\x76\xe0\x36\xc8\x56\x4c\x91\x57\x4f\x99\xaa\x71\xad\x7b\xfa\x00\xd6\x3a\x71\x6a\x59\xf5\xb9\x7d\x67\x8f\x9b\xa1\xd8\x99\xc7\x4f\x24\xfa\xb9\x3e\x06\x8a\x97\xfa\xe2\x01\x66\x7e\xe1\x9b\x8a\x5d\x76\xf0\x96\x3a\x8c\xaa\x78\x67\x62\x73\x1e\x1e\x16\x75\x8d\x1a\x1e\x20\x75\x8e\x66\xc3\xb0\x5d\x37\x0d\x27\x51\x94\x8a\xab\xe9\x72\x7d\x4f\x94\x4a\xd6\xfd\x59\x1e\x55\x75\x74\x27\x5a\xf7\x7b\x7b\x4b\xfd\xa8\x28\x5e\x74\xa6\x50\x2d\x8f\xcb\x39\x76\xa2\x2e\x3b\xb6\x99\x42\x8d\x17\x87\x1e\x6e\x01\x32\xce\x9e\x9a\x4e\x19\xeb\x94\x78\x2e\xef\x38\x76\x0e\x17\x4a\x24\xf9\x94\xda\xaf\x12\xcd\x63\x69\x63\xea\x20\xb9\xf6\x7f\xbf\x34\x4f\x88\x4b\x05\x72\xbc\xbf\x4f\xd4\xff\xc3\xfe\x5f\xb4\x73\xdd\x75\x8e\xf1\x65\xe5\x28\x77\x30\x76\xa6\x7f\xaf\xd2\x39\x25\xd0\xaf\x66\x4c\xd1\x87\x04\x1a\x7d\x8a\x27\xfa\xd5\xe2\x89\xde\xa6\x71\x94\xe1\x9d\x13\x8f\x0f\x28\x6a\x8c\xf6\xf9\xfd\xc3\x0d\x9c\x4e\xf4\x72\x0b\x70\xf8\x88\xe8\x23\x53\x91\x37\xae\xc1\x2a\x1f\x0c\x73\x5b\x40\x0f\x70\x5e\xec\xe2\x06\xd9\xc5\xc1\xa1\x2f\x4c\xd8\xea\x78\xd9\xd5\xc0\xab\x5d\x53\xb1\x0d\x5c\xc1\x10\xeb\x6e\x0a\x97\x47\xc5\xba\xbb\xa2\x7a\x65\xc4\x96\x02\xfa\x56\x8a\xea\x65\x14\xf7\x97\xf9\x7e\xab\x73\x42\xc9\x2e\x87\x1d\x6e\xda\xf4\xbb\x79\x2e\xe4\xed\x13\x28\x7f\x1a\x1c\x66\x8d\xb6\x82\xbc\xcc\xc1\xc9\x9f\xf5\x2c\x7d\x7b\xbf\xd3\x01\xb7\x93\xab\xcf\xe5\x0f\xdb\x58\xd6\x8f\xe1\x99\x2a\x2c\x33\xad\xe6\x52\x7e\x68\xd8\xd9\xa7\x90\xbe\x6a\x48\xdf\xbf\x7d\x20\xde\xe3\x22\xdc\xac\x9d\x26\xf2\x01\xc1\x7c\x15\xe3\xcf\xbe\xed\x44\x19\x8a\x0f\x0d\x5b\x13\x48\xab\xd7\xd8\xa1\xee\xe8\x0c\x8b\xdb\x2d\x70\xed\x77\x8c\xaa\xfa\x68\x51\x6e\xda\x1f\x51\xf3\x00\x19\x52\x29\x74\x11\xaf\x36\xbb\x4a\xd9\xe4\x9c\x1d\x13\x27\x09\x8a\xb7\xf5\xe6\xc8\xa4\x7e\x75\x83\xed\xac\x9c\xdd\xfe\xeb\x7a\x22\x03\xf7\x26\x03\xcc\xdc\xdd\xe3\xd4\x54\x84\xf7\xae\xdb\x05\x2f\xab\x1b\x15\x8d\x9b\x24\x95\x5d\x0b\x01\x40\x72\x72\x8b\xb7\xca\x04\x0f\x20\xf1\x91\x2b\xe7\xe6\x63\x84\x54\xee\x18\x10\x69\xce\x26\x33\x3c\xf2\xb7\x89\x51\xac\x2f\xe6\x76\x83\x1a\x16\xf6\xda\xd8\xdc\x3e\xb6\xa3\x3f\x28\x8e\x71\x17\xc1\xbc\x43\x70\x62\xd5\x87\xfa\x51\x62\x13\x1b\xe5\xfb\xe3\x5a\xf1\x90\x48\xc3\xdf\x26\xc0\x50\x07\x34\x3a\x24\xb5\xcc\xf8\x48\x5e\x0d\xb7\xb5\x0a\x5d\x68\x5d\xc1\x73\x5b\x73\x37\xf0\xbb\x47\x54\xc2\xb7\xeb\xc2\x8a\x01\x94\x76\xce\x93\x91\xd3\xd5\x61\x5f\xf7\x66\x07\x1f\x8e\xfc\x36\xb1\x2f\x74\x53\x91\x7e\x95\x9b\xca\x82\xf6\xb3\x17\x19\x18\x85\xcf\xda\xc4\x71\xc3\x59\xd0\x4e\xd2\xec\x22\x8a\x6b\x99\x28\xe3\x46\x43\xe2\xb8\xd4\x2c\x68\xff\xe7\xf3\xe7\xcf\xdb\xdb\x22\x11\x3f\xfb\x14\x89\xf8\x6b\x45\x22\xfe\xbb\xc5\x18\x36\x86\x00\x9a\xa1\x4e\xbf\x97\xd1\xf6\x29\x2a\xf0\x8f\x1f\x15\xc8\xaf\x60\x34\xb7\xfa\xb4\xa3\x16\x0c\x99\x34\xb4\x97\x49\xfb\x86\xc9\x84\xdc\xbb\x34\x52\xdf\x47\x47\xfb\xc7\x0e\x3c\xbc\x76\xd9\x6e\xf0\x39\x33\x08\x52\xca\x4e\x0a\x96\xf1\xcd\x0e\xf0\x37\x1a\xfe\xe6\x5f\x29\xb0\xb1\x21\xa2\xf0\x77\x8c\xd2\x6b\xbc\x05\x48\x3a\x3c\x2a\x8f\xff\xdb\x51\x95\xe5\x1d\xcf\xf5\xa1\xe1\xfa\x55\x52\xbd\x8e\xba\x66\x76\x92\xd4\x7c\x12\xc6\x93\x17\xbb\xf6\x4c\xdd\x6a\x90\xf8\x24\x2a\xa3\xf1\xbc\x74\x1a\xba\x71\xdd\xdd\x0d\xa7\x29\x9e\x4d\x4f\xc3\x21\x32\x32\x88\x75\x7e\xc3\xe6\x41\x58\x51\xa4\xe4\x45\xd0\x7a\x2f\xb9\xa1\x87\xa1\x93\x22\x65\xc3\x8a\x1e\x3e\x4e\xb9\x44\x8b\x1a\x23\x0a\xcb\x8a\x3e\x39\xb1\x3f\x39\xb1\x77\x71\x62\x57\xf4\xe2\x8a\xe8\x3b\x77\x82\x3a\x08\xab\x6a\xe6\x33\x37\x22\x45\x9b\x4b\x15\xae\x2b\x35\x36\x80\x4b\xab\xde\x52\x86\x33\xa2\x53\x89\xdf\x52\x48\xf2\xa5\x4b\xb9\xdf\x56\x15\x9f\x18\x0f\x8d\xa6\xfd\xcd\xf7\x00\x62\x39\x87\xd4\xcb\xb6\xf6\x9e\x80\xd6\x9a\x87\x27\xfc\xbb\xb2\x49\x20\x83\x22\x1c\x8f\x8f\xd4\x3d\x0e\x1a\x97\x55\xb6\xfa\x48\xc9\x0e\xae\xbd\x4a\x18\x16\x33\x62\xa2\xdd\x5e\x47\x5e\xf3\x8c\x81\xc6\x3e\x4f\x93\x79\x54\x48\x5a\xfc\x60\x5b\x1c\x77\xcd\x51\x88\x27\xec\x36\xe8\xb8\xfd\xe4\x2b\xfc\xe3\xf8\x0a\x1b\x03\x2a\xdd\x8b\x71\xb7\xeb\xdc\xaf\xb2\x1f\xf9\x6a\xd0\x55\x1b\x54\x85\xfb\x7d\x3b\xea\xa5\xde\x0f\x8c\xe3\xcc\xb9\x16\x9a\x57\x74\x4b\x37\xe6\x8f\x14\x8b\x19\x1b\x2f\xfa\xb0\x7b\x8f\x5b\xdf\x13\x9b\xe9\x78\xdc\xd7\xaa\x45\xbd\x04\xfc\x61\x41\x9b\x4d\xe4\x35\xf8\x79\x77\x3e\xbb\xf2\x38\xff\xad\xf4\x9a\x3e\x74\x23\xf8\x37\xf3\x7f\x6f\xf3\x0e\x3b\xc5\xb5\x4b\xd4\xbb\x05\xf9\x16\x2f\x2c\xa9\x06\xae\xda\x6e\xd9\x9a\x83\xf1\x7e\x96\x78\x74\x64\xe8\xb6\x09\x4f\x77\x98\xdb\x72\x75\xf0\x8d\x20\xd1\x5f\x35\x1e\xd0\xd5\x42\x90\x76\xe2\x95\x82\x5a\x4c\x60\xb0\x25\xc8\xce\x8e\x63\xd3\x21\x77\xcd\x81\x6f\xf2\x39\xff\xed\x67\x9f\xec\x8e\xde\x1e\xfe\x36\xf9\x7f\x01\x00\x00\xff\xff\xc3\x31\x5e\xb1\x7b\xcb\x00\x00") 65 | 66 | func chart_min_js_bytes() ([]byte, error) { 67 | return bindata_read( 68 | _chart_min_js, 69 | "Chart.min.js", 70 | ) 71 | } 72 | 73 | func chart_min_js() (*asset, error) { 74 | bytes, err := chart_min_js_bytes() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | info := bindata_file_info{name: "Chart.min.js", size: 52091, mode: os.FileMode(420), modTime: time.Unix(1430243788, 0)} 80 | a := &asset{bytes: bytes, info: info} 81 | return a, nil 82 | } 83 | 84 | // Asset loads and returns the asset for the given name. 85 | // It returns an error if the asset could not be found or 86 | // could not be loaded. 87 | func Asset(name string) ([]byte, error) { 88 | cannonicalName := strings.Replace(name, "\\", "/", -1) 89 | if f, ok := _bindata[cannonicalName]; ok { 90 | a, err := f() 91 | if err != nil { 92 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 93 | } 94 | return a.bytes, nil 95 | } 96 | return nil, fmt.Errorf("Asset %s not found", name) 97 | } 98 | 99 | // MustAsset is like Asset but panics when Asset would return an error. 100 | // It simplifies safe initialization of global variables. 101 | func MustAsset(name string) []byte { 102 | a, err := Asset(name) 103 | if err != nil { 104 | panic("asset: Asset(" + name + "): " + err.Error()) 105 | } 106 | 107 | return a 108 | } 109 | 110 | // AssetInfo loads and returns the asset info for the given name. 111 | // It returns an error if the asset could not be found or 112 | // could not be loaded. 113 | func AssetInfo(name string) (os.FileInfo, error) { 114 | cannonicalName := strings.Replace(name, "\\", "/", -1) 115 | if f, ok := _bindata[cannonicalName]; ok { 116 | a, err := f() 117 | if err != nil { 118 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 119 | } 120 | return a.info, nil 121 | } 122 | return nil, fmt.Errorf("AssetInfo %s not found", name) 123 | } 124 | 125 | // AssetNames returns the names of the assets. 126 | func AssetNames() []string { 127 | names := make([]string, 0, len(_bindata)) 128 | for name := range _bindata { 129 | names = append(names, name) 130 | } 131 | return names 132 | } 133 | 134 | // _bindata is a table, holding each asset generator, mapped to its name. 135 | var _bindata = map[string]func() (*asset, error){ 136 | "Chart.min.js": chart_min_js, 137 | } 138 | 139 | // AssetDir returns the file names below a certain 140 | // directory embedded in the file by go-bindata. 141 | // For example if you run go-bindata on data/... and data contains the 142 | // following hierarchy: 143 | // data/ 144 | // foo.txt 145 | // img/ 146 | // a.png 147 | // b.png 148 | // then AssetDir("data") would return []string{"foo.txt", "img"} 149 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 150 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 151 | // AssetDir("") will return []string{"data"}. 152 | func AssetDir(name string) ([]string, error) { 153 | node := _bintree 154 | if len(name) != 0 { 155 | cannonicalName := strings.Replace(name, "\\", "/", -1) 156 | pathList := strings.Split(cannonicalName, "/") 157 | for _, p := range pathList { 158 | node = node.Children[p] 159 | if node == nil { 160 | return nil, fmt.Errorf("Asset %s not found", name) 161 | } 162 | } 163 | } 164 | if node.Func != nil { 165 | return nil, fmt.Errorf("Asset %s not found", name) 166 | } 167 | rv := make([]string, 0, len(node.Children)) 168 | for name := range node.Children { 169 | rv = append(rv, name) 170 | } 171 | return rv, nil 172 | } 173 | 174 | type _bintree_t struct { 175 | Func func() (*asset, error) 176 | Children map[string]*_bintree_t 177 | } 178 | 179 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ 180 | "Chart.min.js": &_bintree_t{chart_min_js, map[string]*_bintree_t{}}, 181 | }} 182 | 183 | // Restore an asset under the given directory 184 | func RestoreAsset(dir, name string) error { 185 | data, err := Asset(name) 186 | if err != nil { 187 | return err 188 | } 189 | info, err := AssetInfo(name) 190 | if err != nil { 191 | return err 192 | } 193 | err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755)) 194 | if err != nil { 195 | return err 196 | } 197 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 198 | if err != nil { 199 | return err 200 | } 201 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 202 | if err != nil { 203 | return err 204 | } 205 | return nil 206 | } 207 | 208 | // Restore assets under the given directory recursively 209 | func RestoreAssets(dir, name string) error { 210 | children, err := AssetDir(name) 211 | if err != nil { // File 212 | return RestoreAsset(dir, name) 213 | } else { // Dir 214 | for _, child := range children { 215 | err = RestoreAssets(dir, path.Join(name, child)) 216 | if err != nil { 217 | return err 218 | } 219 | } 220 | } 221 | return nil 222 | } 223 | 224 | func _filePath(dir, name string) string { 225 | cannonicalName := strings.Replace(name, "\\", "/", -1) 226 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 227 | } 228 | -------------------------------------------------------------------------------- /examples/reckoning-multiple-instances/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "flag" 22 | "fmt" 23 | "log" 24 | "net" 25 | "os" 26 | "strconv" 27 | "sync" 28 | 29 | "github.com/zulily/reckon" 30 | ) 31 | 32 | // Address represents a host:port address. 33 | type Address struct { 34 | Host string 35 | Port int 36 | } 37 | 38 | // Addresses is a slice of Address instances. It implements the flag.Value 39 | // interface, and thus can be used with the Var func in the flag pkg 40 | type Addresses []Address 41 | 42 | func (a *Addresses) String() string { 43 | var buf bytes.Buffer 44 | for _, addr := range *a { 45 | buf.WriteString(net.JoinHostPort(addr.Host, strconv.Itoa(addr.Port))) 46 | } 47 | return buf.String() 48 | } 49 | 50 | // Set is part of the flag.Value interface to allow Addresses to be used as 51 | // flag values 52 | func (a *Addresses) Set(value string) error { 53 | host, port, err := net.SplitHostPort(value) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | p, err := strconv.Atoi(port) 59 | if err != nil { 60 | return err 61 | } 62 | *a = append(*a, Address{Host: host, Port: p}) 63 | return nil 64 | } 65 | 66 | // reckonResult allow us to return results OR an error on the same chan 67 | type reckonResult struct { 68 | s map[string]*reckon.Results 69 | keyCount int64 70 | err error 71 | } 72 | 73 | type options struct { 74 | redises Addresses 75 | minSamples int 76 | sampleRate float64 77 | } 78 | 79 | var ( 80 | opts options 81 | ) 82 | 83 | func main() { 84 | 85 | flag.Float64Var(&opts.sampleRate, "sample-rate", 0.1, "The percentage of the keyspace to sample on each redis") 86 | flag.IntVar(&opts.minSamples, "min-samples", 100, "minimum number of keys to sample on each redis") 87 | flag.Var(&opts.redises, "redis", "host:port address of a redis instance to sample (may be specified multiple times)") 88 | flag.Parse() 89 | 90 | // Sample 100 keys from each of three redis instances, all running on different ports on localhost 91 | var reckonOpts []reckon.Options 92 | 93 | for _, redis := range opts.redises { 94 | opt := reckon.Options{Host: redis.Host, Port: redis.Port, MinSamples: opts.minSamples, SampleRate: float32(opts.sampleRate)} 95 | reckonOpts = append(reckonOpts, opt) 96 | } 97 | 98 | aggregator := reckon.AggregatorFunc(reckon.AnyKey) 99 | 100 | var wg sync.WaitGroup 101 | results := make(chan reckonResult) 102 | wg.Add(len(reckonOpts)) 103 | 104 | // Sample each redis in its own goroutine 105 | for _, instanceOpts := range reckonOpts { 106 | go func(opts reckon.Options) { 107 | defer wg.Done() 108 | log.Printf("Sampling %d keys from redis at: %s:%d...\n", opts.MinSamples, opts.Host, opts.Port) 109 | s, keyCount, err := reckon.Run(opts, aggregator) 110 | results <- reckonResult{s: s, keyCount: keyCount, err: err} 111 | }(instanceOpts) 112 | } 113 | 114 | // Collect and merge all the results 115 | totals := make(map[string]*reckon.Results) 116 | totalKeyCount := int64(0) 117 | 118 | go func() { 119 | for r := range results { 120 | if r.err != nil { 121 | panic(r.err) 122 | } 123 | log.Println("Got results back from a redis instance!") 124 | 125 | totalKeyCount += r.keyCount 126 | for k, v := range r.s { 127 | if existing, ok := totals[k]; ok { 128 | existing.Merge(v) 129 | totals[k] = existing 130 | } else { 131 | totals[k] = v 132 | } 133 | } 134 | } 135 | }() 136 | 137 | // render the final results to HTML when everything is complete 138 | wg.Wait() 139 | close(results) 140 | 141 | log.Printf("total key count: %d\n", totalKeyCount) 142 | for k, v := range totals { 143 | 144 | v.Name = k 145 | if f, err := os.Create(fmt.Sprintf("output-%s.html", k)); err != nil { 146 | panic(err) 147 | } else { 148 | defer f.Close() 149 | log.Printf("Rendering totals for: %s to %s:\n", k, f.Name()) 150 | if err := reckon.RenderHTML(v, f); err != nil { 151 | panic(err) 152 | } 153 | } 154 | 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /examples/reckoning-single-instance/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "fmt" 22 | "log" 23 | "os" 24 | "strings" 25 | 26 | "github.com/zulily/reckon" 27 | ) 28 | 29 | // (completely contrived) example Aggregator funcs: 30 | 31 | // aggregateByFirst letter aggregates redis stats according the first letter of the redis key 32 | func aggregateByFirstLetter(key string, valueType reckon.ValueType) []string { 33 | return []string{key[:1]} 34 | } 35 | 36 | // setsThatStartWithA ignores any sampled key that is not a set or does not 37 | // start with the letter 'a'. It aggregates keys that DO meet this criteria up 38 | // to a group named (appropriately) "setsThatStartWithA". 39 | func setsThatStartWithA(key string, valueType reckon.ValueType) []string { 40 | if strings.HasPrefix(key, "a") && valueType == reckon.TypeSet { 41 | return []string{"setsThatStartWithA"} 42 | } 43 | return []string{} 44 | } 45 | 46 | func main() { 47 | 48 | var sampleRate float64 49 | opts := reckon.Options{} 50 | flag.StringVar(&opts.Host, "host", "localhost", "the hostname of the redis server") 51 | flag.IntVar(&opts.Port, "port", 6379, "the port of the redis server") 52 | flag.IntVar(&opts.MinSamples, "min-samples", 50, "number of random samples to take (should be <= the number of keys in the redis instance") 53 | flag.Float64Var(&sampleRate, "sample-rate", 0.1, "The percentage of the keyspace to sample on each redis") 54 | flag.Parse() 55 | 56 | opts.SampleRate = float32(sampleRate) 57 | stats, keyCount, err := reckon.Run(opts, reckon.AggregatorFunc(reckon.AnyKey)) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | log.Printf("total key count: %d\n", keyCount) 63 | for k, v := range stats { 64 | log.Printf("stats for: %s\n", k) 65 | 66 | v.Name = k 67 | if f, err := os.Create(fmt.Sprintf("output-%s.html", k)); err != nil { 68 | panic(err) 69 | } else { 70 | defer f.Close() 71 | log.Printf("Rendering totals for: '%s' to %s:\n", k, f.Name()) 72 | if err := reckon.RenderHTML(v, f); err != nil { 73 | panic(err) 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /random-sets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zulily/reckon/077b9ca6371f34d118f89961eea9cc7c03c00c24/random-sets.png -------------------------------------------------------------------------------- /reckon.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // Package reckon provides support for sampling and reporting on the keys and 18 | // values in one or more redis instances 19 | package reckon 20 | 21 | import ( 22 | "errors" 23 | "fmt" 24 | "net" 25 | "regexp" 26 | "strconv" 27 | "strings" 28 | 29 | "github.com/garyburd/redigo/redis" 30 | ) 31 | 32 | // Options is a configuration struct that instructs the reckon pkg to sample 33 | // the redis instance listening on a particular host/port with a specified 34 | // number/percentage of random keys. 35 | type Options struct { 36 | Host string 37 | Port int 38 | 39 | // MinSamples indicates the minimum number of random keys to sample from the redis 40 | // instance. Note that this does not mean **unique** keys, just an absolute 41 | // number of random keys. Therefore, this number should be small relative to 42 | // the number of keys in the redis instance. 43 | MinSamples int 44 | 45 | // SampleRate indicates the percentage of the keyspace to sample. 46 | // Accordingly, values should be between 0.0 and 1.0. If a non-zero value is 47 | // given for both `SampleRate` and `MinSamples`, the actual number of keys 48 | // sampled will be the greater of the two values, once the key count has been 49 | // calculated using the `SampleRate`. 50 | SampleRate float32 51 | } 52 | 53 | // A ValueType represents the various data types that redis can store. The 54 | // string representation of a ValueType matches what is returned from redis' 55 | // `TYPE` command. 56 | type ValueType string 57 | 58 | var ( 59 | // TypeString represents a redis string value 60 | TypeString ValueType = "string" 61 | 62 | // TypeSortedSet represents a redis sorted set value 63 | TypeSortedSet ValueType = "zset" 64 | 65 | // TypeSet represents a redis set value 66 | TypeSet ValueType = "set" 67 | 68 | // TypeHash represents a redis hash value 69 | TypeHash ValueType = "hash" 70 | 71 | // TypeList represents a redis list value 72 | TypeList ValueType = "list" 73 | 74 | // TypeUnknown means that the redis value type is undefined, and indicates an error 75 | TypeUnknown ValueType = "unknown" 76 | 77 | // ErrNoKeys is the error returned when a specified redis instance contains 78 | // no keys, or the key count could not be determined 79 | ErrNoKeys = errors.New("No keys are present in the configured redis instance") 80 | 81 | // keysExpr captures the key count from the matching line of output from 82 | // redis' "INFO" command 83 | keysExpr = regexp.MustCompile("^db\\d+:keys=(\\d+),") 84 | ) 85 | 86 | // AnyKey is an AggregatorFunc that puts any sampled key (regardless of key 87 | // name or redis data type) into a generic "any-key" bucket. 88 | func AnyKey(key string, valueType ValueType) []string { 89 | return []string{"any-key"} 90 | } 91 | 92 | // An Aggregator returns 0 or more arbitrary strings, to be used during a 93 | // sampling operation as aggregation groups or "buckets". For example, an 94 | // Aggregator that takes the first letter of the key would cause reckon to 95 | // aggregate stats by each letter of the alphabet 96 | type Aggregator interface { 97 | Groups(key string, valueType ValueType) []string 98 | } 99 | 100 | // The AggregatorFunc type is an adapter to allow the use of 101 | // ordinary functions as Aggregators. If f is a function 102 | // with the appropriate signature, AggregatorFunc(f) is an 103 | // Aggregator object that calls f. 104 | type AggregatorFunc func(key string, valueType ValueType) []string 105 | 106 | // Groups provides 0 or more groups to aggregate `key` to, when sampling redis keys. 107 | func (f AggregatorFunc) Groups(key string, valueType ValueType) []string { 108 | return f(key, valueType) 109 | } 110 | 111 | // flush is a convenience func for flushing a redis pipeline, receiving the 112 | // replies, and returning them, along with any error 113 | func flush(conn redis.Conn) ([]interface{}, error) { 114 | return redis.Values(conn.Do("")) 115 | } 116 | 117 | // ensureEntry is a convenience func for obtaining the Stats instance for the 118 | // specified `group`, creating a new one if no such entry already exists 119 | func ensureEntry(m map[string]*Results, group string, init func() *Results) *Results { 120 | var stats *Results 121 | var ok bool 122 | if stats, ok = m[group]; !ok { 123 | stats = init() 124 | m[group] = stats 125 | } 126 | return stats 127 | } 128 | 129 | // randomKey obtains a random redis key and its ValueType from the supplied redis connection 130 | func randomKey(conn redis.Conn) (key string, vt ValueType, err error) { 131 | key, err = redis.String(conn.Do("RANDOMKEY")) 132 | if err != nil { 133 | return key, TypeUnknown, err 134 | } 135 | 136 | typeStr, err := redis.String(conn.Do("TYPE", key)) 137 | if err != nil { 138 | return key, TypeUnknown, err 139 | } 140 | 141 | return key, ValueType(typeStr), nil 142 | } 143 | 144 | // keyCount obtains a the number of keys in the redis instance. 145 | func keyCount(conn redis.Conn) (count int64, err error) { 146 | resp, err := redis.String(conn.Do("INFO")) 147 | if err != nil { 148 | return count, err 149 | } 150 | 151 | for _, str := range strings.Split(resp, "\n") { 152 | if matches := keysExpr.FindStringSubmatch(str); len(matches) >= 2 { 153 | if count, err = strconv.ParseInt(matches[1], 10, 64); err == nil && count != 0 { 154 | return count, nil 155 | } 156 | return count, ErrNoKeys 157 | } 158 | } 159 | 160 | return 0, ErrNoKeys 161 | } 162 | 163 | func sampleString(key string, conn redis.Conn, aggregator Aggregator, stats map[string]*Results) error { 164 | val, err := redis.String(conn.Do("GET", key)) 165 | if err != nil { 166 | return err 167 | } 168 | 169 | for _, agg := range aggregator.Groups(key, TypeString) { 170 | s := ensureEntry(stats, agg, NewResults) 171 | s.observeString(key, val) 172 | } 173 | return nil 174 | } 175 | 176 | func sampleList(key string, conn redis.Conn, aggregator Aggregator, stats map[string]*Results) error { 177 | // TODO: Let's not always get the first element, like the orig. reckon 178 | conn.Send("LLEN", key) 179 | conn.Send("LRANGE", key, 0, 0) 180 | replies, err := flush(conn) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | if len(replies) >= 2 { 186 | l, err := redis.Int(replies[0], nil) 187 | ms, err := redis.Strings(replies[1], err) 188 | if err != nil { 189 | return err 190 | } 191 | 192 | for _, g := range aggregator.Groups(key, TypeList) { 193 | s := ensureEntry(stats, g, NewResults) 194 | s.observeList(key, l, ms[0]) 195 | } 196 | } 197 | return nil 198 | } 199 | 200 | func sampleSet(key string, conn redis.Conn, aggregator Aggregator, stats map[string]*Results) error { 201 | conn.Send("SCARD", key) 202 | conn.Send("SRANDMEMBER", key) 203 | replies, err := flush(conn) 204 | if err != nil { 205 | return err 206 | } 207 | 208 | if len(replies) >= 2 { 209 | l, err := redis.Int(replies[0], nil) 210 | m, err := redis.String(replies[1], err) 211 | if err != nil { 212 | return err 213 | } 214 | 215 | for _, g := range aggregator.Groups(key, TypeSet) { 216 | s := ensureEntry(stats, g, NewResults) 217 | s.observeSet(key, l, m) 218 | } 219 | } 220 | return nil 221 | } 222 | 223 | func sampleSortedSet(key string, conn redis.Conn, aggregator Aggregator, stats map[string]*Results) error { 224 | conn.Send("ZCARD", key) 225 | // TODO: Let's not always get the first element, like the orig. sampler 226 | conn.Send("ZRANGE", key, 0, 0) 227 | replies, err := flush(conn) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | if len(replies) >= 2 { 233 | l, err := redis.Int(replies[0], nil) 234 | ms, err := redis.Strings(replies[1], err) 235 | if err != nil { 236 | return err 237 | } 238 | 239 | for _, g := range aggregator.Groups(key, TypeSortedSet) { 240 | s := ensureEntry(stats, g, NewResults) 241 | s.observeSortedSet(key, l, ms[0]) 242 | } 243 | } 244 | return nil 245 | } 246 | 247 | func sampleHash(key string, conn redis.Conn, aggregator Aggregator, stats map[string]*Results) error { 248 | conn.Send("HLEN", key) 249 | conn.Send("HKEYS", key) 250 | replies, err := flush(conn) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | if len(replies) >= 2 { 256 | for _, g := range aggregator.Groups(key, TypeHash) { 257 | 258 | // TODO: Let's not always get the first hash field, like the orig. sampler 259 | l, err := redis.Int(replies[0], nil) 260 | fields, err := redis.Strings(replies[1], err) 261 | if err != nil { 262 | return err 263 | } 264 | val, err := redis.String(conn.Do("HGET", key, fields[0])) 265 | if err != nil { 266 | return err 267 | } 268 | s := ensureEntry(stats, g, NewResults) 269 | s.observeHash(key, l, fields[0], val) 270 | } 271 | } 272 | return nil 273 | } 274 | 275 | func max(a, b int) int { 276 | if a > b { 277 | return a 278 | } 279 | return b 280 | } 281 | 282 | // Run performs the configured sampling operation against the redis instance, 283 | // returning aggregated statistics using the provided Aggregator, as well as 284 | // the actual key count for the redis instance. If any errors occur, the 285 | // sampling is short-circuited, and the error is returned. In such a case, the 286 | // results should be considered invalid. 287 | func Run(opts Options, aggregator Aggregator) (map[string]*Results, int64, error) { 288 | 289 | stats := make(map[string]*Results) 290 | var err error 291 | var keys int64 292 | 293 | if opts.SampleRate < 0.0 || opts.SampleRate > 1.0 { 294 | return stats, keys, errors.New("SampleRate must be between 0.0 and 1.0") 295 | } 296 | 297 | if opts.MinSamples <= 0 && opts.SampleRate == 0.0 { 298 | return stats, keys, errors.New("MinSamples cannot be 0") 299 | } 300 | 301 | conn, err := redis.Dial("tcp", net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))) 302 | if err != nil { 303 | return stats, keys, fmt.Errorf("Error connecting to the redis instance at: %s:%d : %s", opts.Host, opts.Port, err.Error()) 304 | } 305 | 306 | numSamples := opts.MinSamples 307 | 308 | if keys, err = keyCount(conn); err != nil { 309 | return stats, keys, err 310 | } 311 | 312 | fmt.Printf("redis at %s:%d has %d keys\n", opts.Host, opts.Port, keys) 313 | if opts.SampleRate > 0.0 { 314 | v := int(float32(keys) * opts.SampleRate) 315 | numSamples = max(max(v, numSamples), 1) 316 | } 317 | 318 | interval := numSamples / 100 319 | if interval == 0 { 320 | interval = 1 321 | } 322 | lastInterval := 0 323 | 324 | for i := 0; i < numSamples; i++ { 325 | key, vt, err := randomKey(conn) 326 | if err != nil { 327 | return stats, keys, err 328 | } 329 | 330 | if i/interval != lastInterval { 331 | fmt.Printf("sampled %d keys from redis at: %s:%d...\n", i, opts.Host, opts.Port) 332 | lastInterval = i / interval 333 | } 334 | 335 | switch ValueType(vt) { 336 | case TypeString: 337 | if err = sampleString(key, conn, aggregator, stats); err != nil { 338 | return stats, keys, err 339 | } 340 | case TypeList: 341 | if err = sampleList(key, conn, aggregator, stats); err != nil { 342 | return stats, keys, err 343 | } 344 | case TypeSet: 345 | if err = sampleSet(key, conn, aggregator, stats); err != nil { 346 | return stats, keys, err 347 | } 348 | case TypeSortedSet: 349 | if err = sampleSortedSet(key, conn, aggregator, stats); err != nil { 350 | return stats, keys, err 351 | } 352 | case TypeHash: 353 | if err = sampleHash(key, conn, aggregator, stats); err != nil { 354 | return stats, keys, err 355 | } 356 | default: 357 | return stats, keys, fmt.Errorf("unknown type for redis key: %s", key) 358 | } 359 | } 360 | return stats, keys, nil 361 | } 362 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package reckon 18 | 19 | import "math" 20 | 21 | const ( 22 | // MaxExampleKeys sets an upper bound on the number of example keys that will 23 | // be captured during sampling 24 | MaxExampleKeys = 10 25 | // MaxExampleElements sets an upper bound on the number of example elements that 26 | // will be captured during sampling 27 | MaxExampleElements = 10 28 | // MaxExampleValues sets an upper bound on the number of example values that 29 | // will be captured during sampling 30 | MaxExampleValues = 10 31 | ) 32 | 33 | // Statistics are basic descriptive statistics that summarize data in a frequency table 34 | type Statistics struct { 35 | Mean float64 36 | Min int 37 | Max int 38 | StdDev float64 39 | } 40 | 41 | // NewStatistics creates a new zero-valued Statistics instance 42 | func NewStatistics() *Statistics { 43 | return &Statistics{ 44 | Mean: math.NaN(), 45 | StdDev: math.NaN(), 46 | } 47 | } 48 | 49 | // powerOfTwo returns the smallest power of two that is greater than or equal to `n` 50 | func powerOfTwo(n int) int { 51 | p := 1 52 | for p < n { 53 | p = p * 2 54 | } 55 | return p 56 | } 57 | 58 | // ComputePowerOfTwoFreq converts a frequency map into a new frequency map, 59 | // where each map key is the smallest power of two that is greater than or 60 | // equal to the original map key. 61 | func ComputePowerOfTwoFreq(m map[int]int64) map[int]int64 { 62 | pf := make(map[int]int64) 63 | 64 | for k, v := range m { 65 | p := powerOfTwo(k) 66 | if existing, ok := pf[p]; ok { 67 | pf[p] = existing + v 68 | } else { 69 | pf[p] = v 70 | } 71 | } 72 | 73 | return pf 74 | } 75 | 76 | // ComputeStatistics computes basic descriptive statistics about a frequency map 77 | func ComputeStatistics(m map[int]int64) Statistics { 78 | stats := NewStatistics() 79 | if len(m) == 0 { 80 | return *stats 81 | } 82 | 83 | min := math.MaxInt32 84 | max := math.MinInt32 85 | accum, count, sd := int64(0), int64(0), float64(0) 86 | 87 | for k, v := range m { 88 | if k < min { 89 | min = k 90 | } 91 | if k > max { 92 | max = k 93 | } 94 | accum += int64(k) * v 95 | count += v 96 | } 97 | 98 | mean := float64(accum) / float64(count) 99 | 100 | for k, v := range m { 101 | kf, vf := float64(k), float64(v) 102 | sd += ((kf - mean) * (kf - mean)) * vf 103 | } 104 | 105 | return Statistics{ 106 | Mean: mean, 107 | Min: min, 108 | Max: max, 109 | StdDev: math.Sqrt(sd / float64(count-1)), 110 | } 111 | } 112 | 113 | // add adds `elem` to the "set" (a map[]bool is an idiomatic golang "set") if the 114 | // current size of the set is less than `maxsize` 115 | func add(set map[string]bool, elem string, maxsize int) { 116 | if len(set) >= maxsize { 117 | return 118 | } 119 | set[elem] = true 120 | } 121 | 122 | // Results stores data about sampled redis data structures. Map keys represent 123 | // lengths/sizes, while map values represent the frequency with which those 124 | // lengths/sizes occurred in the sampled data. Example keys are stored in 125 | // golang "sets", which are maps with bool values. 126 | type Results struct { 127 | Name string 128 | KeyCount int64 129 | 130 | // Strings 131 | StringSizes map[int]int64 132 | StringKeys map[string]bool 133 | StringValues map[string]bool 134 | 135 | // Sets 136 | SetSizes map[int]int64 137 | SetElementSizes map[int]int64 138 | SetKeys map[string]bool 139 | SetElements map[string]bool 140 | 141 | // Sorted Sets 142 | SortedSetSizes map[int]int64 143 | SortedSetElementSizes map[int]int64 144 | SortedSetKeys map[string]bool 145 | SortedSetElements map[string]bool 146 | 147 | // Hashes 148 | HashSizes map[int]int64 149 | HashElementSizes map[int]int64 150 | HashValueSizes map[int]int64 151 | HashKeys map[string]bool 152 | HashElements map[string]bool 153 | HashValues map[string]bool 154 | 155 | // Lists 156 | ListSizes map[int]int64 157 | ListElementSizes map[int]int64 158 | ListKeys map[string]bool 159 | ListElements map[string]bool 160 | } 161 | 162 | // NewResults constructs a new, zero-valued Results struct 163 | func NewResults() *Results { 164 | return &Results{ 165 | StringSizes: make(map[int]int64), 166 | StringKeys: make(map[string]bool), 167 | StringValues: make(map[string]bool), 168 | 169 | SetSizes: make(map[int]int64), 170 | SetElementSizes: make(map[int]int64), 171 | SetKeys: make(map[string]bool), 172 | SetElements: make(map[string]bool), 173 | 174 | SortedSetSizes: make(map[int]int64), 175 | SortedSetElementSizes: make(map[int]int64), 176 | SortedSetKeys: make(map[string]bool), 177 | SortedSetElements: make(map[string]bool), 178 | 179 | HashSizes: make(map[int]int64), 180 | HashElementSizes: make(map[int]int64), 181 | HashValueSizes: make(map[int]int64), 182 | HashKeys: make(map[string]bool), 183 | HashElements: make(map[string]bool), 184 | HashValues: make(map[string]bool), 185 | 186 | ListSizes: make(map[int]int64), 187 | ListElementSizes: make(map[int]int64), 188 | ListKeys: make(map[string]bool), 189 | ListElements: make(map[string]bool), 190 | } 191 | } 192 | 193 | // merge inserts all key/value pairs in `b` into `a`. If `b` contains keys 194 | // that are present in `a`, their values will be summed 195 | func merge(a map[int]int64, b map[int]int64) { 196 | for k, v := range b { 197 | a[k] += v 198 | } 199 | } 200 | 201 | // union performs a set union of `a` and `b`, storing the results in `a` 202 | func union(a map[string]bool, b map[string]bool) { 203 | for k := range b { 204 | a[k] = true 205 | } 206 | } 207 | 208 | // trim creates a new set, consisting of up to `n` random members from set `s`. 209 | // If `len(s)` < `n`, the returned map will be of length `len(s)`. Set `s` 210 | // remains unmodified. 211 | func trim(s map[string]bool, n int) map[string]bool { 212 | t := make(map[string]bool) 213 | // map iteration is random in golang! 214 | for k := range s { 215 | t[k] = true 216 | if len(t) == n { 217 | break 218 | } 219 | } 220 | return t 221 | } 222 | 223 | // trimAndSum removes entries from the frequency map that comprise less than 224 | // `threshold` % of the total, returning the sum of the **original** map 225 | func trimAndSum(m map[int]int64, threshold float64) int64 { 226 | var s int64 227 | var sum float64 228 | for _, v := range m { 229 | s += v 230 | } 231 | sum = float64(s) 232 | for k, v := range m { 233 | if float64(v)/sum <= threshold { 234 | delete(m, k) 235 | } 236 | } 237 | return s 238 | } 239 | 240 | // Merge adds the results from `other` into the method receiver. This method 241 | // can be used to combine sampling results from multiple redis instances into a 242 | // single result set. 243 | func (r *Results) Merge(other *Results) { 244 | r.KeyCount += other.KeyCount 245 | 246 | // union all sets 247 | union(r.StringKeys, other.StringKeys) 248 | union(r.StringValues, other.StringValues) 249 | union(r.SetKeys, other.SetKeys) 250 | union(r.SetElements, other.SetElements) 251 | union(r.SortedSetKeys, other.SortedSetKeys) 252 | union(r.SortedSetElements, other.SortedSetElements) 253 | union(r.HashKeys, other.HashKeys) 254 | union(r.HashElements, other.HashElements) 255 | union(r.HashValues, other.HashValues) 256 | union(r.ListKeys, other.ListKeys) 257 | union(r.ListElements, other.ListElements) 258 | 259 | // merge all frequency tables 260 | merge(r.StringSizes, other.StringSizes) 261 | merge(r.SetSizes, other.SetSizes) 262 | merge(r.SetElementSizes, other.SetElementSizes) 263 | merge(r.SortedSetSizes, other.SortedSetSizes) 264 | merge(r.SortedSetElementSizes, other.SortedSetElementSizes) 265 | merge(r.HashSizes, other.HashSizes) 266 | merge(r.HashElementSizes, other.HashElementSizes) 267 | merge(r.HashValueSizes, other.HashValueSizes) 268 | merge(r.ListSizes, other.ListSizes) 269 | merge(r.ListElementSizes, other.ListElementSizes) 270 | } 271 | 272 | func (r *Results) observeSet(key string, length int, member string) { 273 | r.KeyCount++ 274 | r.SetSizes[length]++ 275 | r.SetElementSizes[len(member)]++ 276 | add(r.SetKeys, key, MaxExampleKeys) 277 | add(r.SetElements, member, MaxExampleElements) 278 | } 279 | 280 | func (r *Results) observeSortedSet(key string, length int, member string) { 281 | r.KeyCount++ 282 | r.SortedSetSizes[length]++ 283 | r.SortedSetElementSizes[len(member)]++ 284 | add(r.SortedSetKeys, key, MaxExampleKeys) 285 | add(r.SortedSetElements, member, MaxExampleElements) 286 | } 287 | 288 | func (r *Results) observeHash(key string, length int, field string, value string) { 289 | r.KeyCount++ 290 | r.HashSizes[length]++ 291 | r.HashValueSizes[len(value)]++ 292 | r.HashElementSizes[len(field)]++ 293 | add(r.HashKeys, key, MaxExampleKeys) 294 | add(r.HashElements, field, MaxExampleElements) 295 | add(r.HashValues, value, MaxExampleValues) 296 | } 297 | 298 | func (r *Results) observeList(key string, length int, member string) { 299 | r.KeyCount++ 300 | r.ListSizes[length]++ 301 | r.ListElementSizes[len(member)]++ 302 | add(r.ListKeys, key, MaxExampleKeys) 303 | add(r.ListElements, member, MaxExampleElements) 304 | } 305 | 306 | func (r *Results) observeString(key, value string) { 307 | r.KeyCount++ 308 | r.StringSizes[len(value)]++ 309 | add(r.StringKeys, key, MaxExampleKeys) 310 | add(r.StringValues, value, MaxExampleValues) 311 | } 312 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package reckon 18 | 19 | import ( 20 | "math" 21 | "testing" 22 | ) 23 | 24 | func assertInt(t *testing.T, expected, actual int) { 25 | if expected != actual { 26 | t.Errorf("expected: %d, actual: %d", expected, actual) 27 | } 28 | } 29 | 30 | func assertFloat(t *testing.T, expected, actual, epsilon float64) { 31 | if math.Abs(expected-actual) >= epsilon { 32 | t.Errorf("expected: %.6f, actual: %.6f", expected, actual) 33 | } 34 | } 35 | 36 | func assertNaN(t *testing.T, actual float64) { 37 | if !math.IsNaN(actual) { 38 | t.Errorf("expected NaN, actual: %.6f", actual) 39 | } 40 | } 41 | 42 | const ( 43 | epsilon float64 = 0.00001 44 | ) 45 | 46 | func TestStatistics(t *testing.T) { 47 | 48 | m := make(map[int]int64) 49 | m[-1] = 1 50 | m[13] = 1 51 | m[67] = 1 52 | m[999] = 1 53 | m[342] = 1 54 | 55 | stats := ComputeStatistics(m) 56 | 57 | assertInt(t, 999, stats.Max) 58 | assertInt(t, -1, stats.Min) 59 | assertFloat(t, 284.0, stats.Mean, epsilon) 60 | assertFloat(t, 423.18554, stats.StdDev, epsilon) 61 | 62 | m = make(map[int]int64) 63 | m[45] = 4 64 | m[123] = 8 65 | m[99999] = 2 66 | m[77] = 1 67 | 68 | stats = ComputeStatistics(m) 69 | 70 | assertInt(t, 99999, stats.Max) 71 | assertInt(t, 45, stats.Min) 72 | assertFloat(t, 13415.93333, stats.Mean, epsilon) 73 | assertFloat(t, 35152.65287, stats.StdDev, epsilon) 74 | } 75 | 76 | func TestStatisticsZeroValues(t *testing.T) { 77 | 78 | m := make(map[int]int64) 79 | stats := ComputeStatistics(m) 80 | 81 | assertInt(t, 0, stats.Max) 82 | assertInt(t, 0, stats.Min) 83 | assertNaN(t, stats.Mean) 84 | assertNaN(t, stats.StdDev) 85 | } 86 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package reckon 18 | 19 | import ( 20 | "fmt" 21 | "io" 22 | "text/template" 23 | ) 24 | 25 | func summarize(m map[int]int64) int64 { 26 | // trim off entries that constitute < 1% of the total 27 | return trimAndSum(m, 0.01) 28 | } 29 | 30 | func fmtFloat(n float64) string { 31 | return fmt.Sprintf("%.2f", n) 32 | } 33 | 34 | func percentage(n, total int64) string { 35 | return fmt.Sprintf("%.2f", 100.0*float64(n)/float64(total)) 36 | } 37 | 38 | // chartJS returns the static js what we need on the HTML templates in order to 39 | // render charts. The js itself has been turned into Go src using go-bindata. 40 | // This func panics if there is any error accessing the embedded asset data. 41 | func chartJS() string { 42 | data, err := Asset("Chart.min.js") 43 | if err != nil { 44 | panic(err) 45 | } 46 | return string(data) 47 | } 48 | 49 | type chartData struct { 50 | DOMElement string 51 | Data map[int]int64 52 | } 53 | 54 | func barChart(domElement string, freq map[int]int64) chartData { 55 | return chartData{ 56 | DOMElement: domElement, 57 | Data: freq, 58 | } 59 | } 60 | 61 | // RenderHTML renders an HTML report for a Results instance to the supplied 62 | // io.Writer 63 | func RenderHTML(s *Results, out io.Writer) error { 64 | 65 | s.StringKeys = trim(s.StringKeys, MaxExampleKeys) 66 | s.StringValues = trim(s.StringValues, MaxExampleValues) 67 | s.SetKeys = trim(s.SetKeys, MaxExampleKeys) 68 | s.SetElements = trim(s.SetElements, MaxExampleElements) 69 | s.SortedSetKeys = trim(s.SortedSetKeys, MaxExampleKeys) 70 | s.SortedSetElements = trim(s.SortedSetElements, MaxExampleElements) 71 | s.HashKeys = trim(s.HashKeys, MaxExampleKeys) 72 | s.HashElements = trim(s.HashElements, MaxExampleElements) 73 | s.HashValues = trim(s.HashValues, MaxExampleValues) 74 | s.ListKeys = trim(s.ListKeys, MaxExampleKeys) 75 | s.ListElements = trim(s.ListElements, MaxExampleElements) 76 | 77 | fm := template.FuncMap{ 78 | "summarize": summarize, 79 | "percentage": percentage, 80 | "power": ComputePowerOfTwoFreq, 81 | "stats": ComputeStatistics, 82 | "fmtFloat": fmtFloat, 83 | "barChart": barChart, 84 | "chartJS": chartJS, 85 | } 86 | t := template.Must(template.New("htmloutput").Funcs(fm).Parse(htmlTmpl)) 87 | return t.ExecuteTemplate(out, "base", s) 88 | } 89 | 90 | // RenderText renders a plaintext report for a Results instance to the supplied 91 | // io.Writer 92 | func RenderText(s *Results, out io.Writer) error { 93 | 94 | s.StringKeys = trim(s.StringKeys, MaxExampleKeys) 95 | s.StringValues = trim(s.StringValues, MaxExampleValues) 96 | s.SetKeys = trim(s.SetKeys, MaxExampleKeys) 97 | s.SetElements = trim(s.SetElements, MaxExampleElements) 98 | s.SortedSetKeys = trim(s.SortedSetKeys, MaxExampleKeys) 99 | s.SortedSetElements = trim(s.SortedSetElements, MaxExampleElements) 100 | s.HashKeys = trim(s.HashKeys, MaxExampleKeys) 101 | s.HashElements = trim(s.HashElements, MaxExampleElements) 102 | s.HashValues = trim(s.HashValues, MaxExampleValues) 103 | s.ListKeys = trim(s.ListKeys, MaxExampleKeys) 104 | s.ListElements = trim(s.ListElements, MaxExampleElements) 105 | 106 | fm := template.FuncMap{ 107 | "summarize": summarize, 108 | "percentage": percentage, 109 | "power": ComputePowerOfTwoFreq, 110 | "stats": ComputeStatistics, 111 | "fmtFloat": fmtFloat, 112 | } 113 | t := template.Must(template.New("output").Funcs(fm).Parse(statsTempl)) 114 | return t.ExecuteTemplate(out, "base", s) 115 | } 116 | -------------------------------------------------------------------------------- /templates_html.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package reckon 18 | 19 | const ( 20 | htmlTmpl = ` 21 | {{define "base"}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | reckoning 30 | 31 | 32 | 41 | 42 | 43 | 44 | 45 |
    46 |
    47 |

    {{.Name}} {{.KeyCount}} keys

    48 |
    49 | 50 | {{ if .StringKeys }} 51 |

    Strings {{summarize .StringSizes}}

    52 |
    53 |
    54 |

    Example keys:

    {{template "examples" .StringKeys}} 55 |

    Value Sizes: {{template "stats" .StringSizes}}

    56 | {{template "freq" .StringSizes}} 57 | {{template "barchart" barChart "StringSizes" .StringSizes}} 58 |

    2n Value Sizes:

    59 | {{template "freq" power .StringSizes}} 60 |
    61 |
    62 | {{ end }} 63 | 64 | {{ if .SetKeys }} 65 |

    Sets {{summarize .SetSizes}}

    66 |
    67 |
    68 |

    Example keys:

    {{template "examples" .SetKeys}} 69 |

    Sizes: {{template "stats" .SetSizes}}

    70 | {{template "freq" .SetSizes}} 71 | {{template "barchart" barChart "SetSizes" .SetSizes}} 72 |

    2n Sizes:

    73 | {{template "freq" power .SetSizes}} 74 | 75 |

    Example elements:

    {{template "examples" .SetElements}} 76 |

    Element Sizes: {{template "stats" .SetElementSizes}}

    77 | {{template "freq" .SetElementSizes}} 78 | {{template "barchart" barChart "SetElementSizes" .SetElementSizes}} 79 |

    2n Element Sizes:

    80 | {{template "freq" power .SetElementSizes}} 81 |
    82 |
    83 | {{ end }} 84 | 85 | {{ if .SortedSetKeys }} 86 |

    Sorted Sets {{summarize .SortedSetSizes}}

    87 |
    88 |
    89 |

    Example keys:

    {{template "examples" .SortedSetKeys}} 90 |

    Sizes: {{template "stats" .SortedSetSizes}}

    91 | {{template "freq" .SortedSetSizes}} 92 | {{template "barchart" barChart "SortedSetSizes" .SortedSetSizes}} 93 |

    2n Sizes:

    94 | {{template "freq" power .SortedSetSizes}} 95 | 96 |

    Example elements:

    {{template "examples" .SortedSetElements}} 97 |

    Element Sizes: {{template "stats" .SortedSetElementSizes}}

    98 | {{template "freq" .SortedSetElementSizes}} 99 | {{template "barchart" barChart "SortedSetElementSizes" .SortedSetElementSizes}} 100 |

    2n Element Sizes:

    101 | {{template "freq" power .SortedSetElementSizes}} 102 |
    103 |
    104 | {{ end }} 105 | 106 | {{ if .ListKeys }} 107 |

    Lists {{summarize .ListSizes}}

    108 |
    109 |
    110 |

    Example keys:

    {{template "examples" .ListKeys}} 111 |

    Sizes: {{template "stats" .ListSizes}}

    112 | {{template "freq" .ListSizes}} 113 | {{template "barchart" barChart "ListSizes" .ListSizes}} 114 |

    2n Sizes:

    115 | {{template "freq" power .ListSizes}} 116 | 117 |

    Example elements:

    {{template "examples" .ListElements}} 118 |

    Element Sizes: {{template "stats" .ListElementSizes}}

    119 | {{template "freq" .ListElementSizes}} 120 | {{template "barchart" barChart "ListElementSizes" .ListElementSizes}} 121 |

    2n Element Sizes:

    122 | {{template "freq" power .ListElementSizes}} 123 |
    124 |
    125 | {{ end }} 126 | 127 | {{ if .HashKeys}} 128 |

    Hashes {{summarize .HashSizes}}

    129 |
    130 |
    131 |

    Example keys:

    {{template "examples" .HashKeys}} 132 |

    Sizes: {{template "stats" .HashSizes}}

    133 | {{template "freq" .HashSizes}} 134 | {{template "barchart" barChart "HashSizes" .HashSizes}} 135 |

    2n Sizes:

    136 | {{template "freq" power .HashSizes}} 137 | 138 |

    Example elements:

    {{template "examples" .HashElements}} 139 |

    Element Sizes: {{template "stats" .HashElementSizes}}

    140 | {{template "freq" .HashElementSizes}} 141 | {{template "barchart" barChart "HashElementSizes" .HashElementSizes}} 142 |

    2n Element Sizes:

    143 | {{template "freq" power .HashElementSizes}} 144 | 145 |

    Example values:

    {{template "examples" .HashValues}} 146 |

    Value Sizes: {{template "stats" .HashValueSizes}}

    147 | {{template "freq" .HashValueSizes}} 148 | {{template "barchart" barChart "HashValueSizes" .HashValueSizes}} 149 |

    2n Value Sizes:

    150 | {{template "freq" power .HashValueSizes}} 151 |
    152 |
    153 | {{ end }} 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | {{end}} 163 | 164 | {{define "barchart"}} 165 | {{/* Supplies the necessary DOM elements and JS to render a simple bar chart (if there are >= 4 data points) */}} 166 | 167 | {{ $l := len .Data }} 168 | {{ if ge $l 4}} 169 | {{ $total := summarize .Data }} 170 | 171 |
    172 | 173 |
    174 | 193 | {{end}} 194 | {{end}} 195 | 196 | {{define "stats"}} 197 | {{ with stats . }} 198 | (min: {{.Min}} max: {{.Max}} mean: {{fmtFloat .Mean}} std dev: {{fmtFloat .StdDev}}) 199 | {{end}} 200 | {{end}} 201 | 202 | {{define "examples"}} 203 |
      204 | {{range $k, $v := .}} 205 |
    • {{$k}}
    • 206 | {{end}} 207 | {{end}} 208 | 209 | {{define "freq"}} 210 | {{ $ss := summarize . }} 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | {{ range $s, $c := .}} 221 | 222 | {{end}} 223 | 224 |
      Size# of occurrences%
      {{$s}} {{$c}} {{percentage $c $ss}}%
      225 | {{end}} 226 | 227 | ` 228 | ) 229 | -------------------------------------------------------------------------------- /templates_text.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 zulily, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package reckon 18 | 19 | const ( 20 | statsTempl = ` 21 | {{define "base"}} 22 | # of keys sampled: {{.KeyCount}} 23 | 24 | {{ if .StringKeys }} 25 | --- Strings ({{summarize .StringSizes}}) --- 26 | {{template "exampleKeys" .StringKeys}} 27 | {{template "exampleValues" .StringValues}} 28 | Sizes ({{template "stats" .StringSizes}}): 29 | {{template "freq" .StringSizes}} 30 | ^2 Sizes:{{template "freq" power .StringSizes}}{{end}} 31 | 32 | {{ if .SetKeys }} 33 | --- Sets ({{summarize .SetSizes}}) --- 34 | {{template "exampleKeys" .SetKeys}} 35 | Sizes ({{template "stats" .SetSizes}}): 36 | {{template "freq" .SetSizes}} 37 | ^2 Sizes:{{template "freq" power .SetSizes}} 38 | {{template "exampleElements" .SetElements}} 39 | Element Sizes:{{template "freq" .SetElementSizes}} 40 | Element ^2 Sizes:{{template "freq" power .SetElementSizes}}{{end}} 41 | 42 | {{ if .SortedSetKeys }} 43 | --- Sorted Sets ({{summarize .SortedSetSizes}}) --- 44 | {{template "exampleKeys" .SortedSetKeys}} 45 | Sizes ({{template "stats" .SortedSetSizes}}): 46 | {{template "freq" .SortedSetSizes}} 47 | ^2 Sizes:{{template "freq" power .SortedSetSizes}} 48 | {{template "exampleElements" .SortedSetElements}} 49 | Element Sizes ({{template "stats" .SortedSetElementSizes}}): 50 | {{template "freq" .SortedSetElementSizes}} 51 | Element ^2 Sizes:{{template "freq" power .SortedSetElementSizes}}{{end}} 52 | 53 | {{ if .HashKeys }} 54 | --- Hashes ({{summarize .HashSizes}}) --- 55 | {{template "exampleKeys" .HashKeys}} 56 | Sizes ({{template "stats" .HashSizes}}): 57 | {{template "freq" .HashSizes}} 58 | ^2 Sizes:{{template "freq" power .HashSizes}} 59 | {{template "exampleElements" .HashElements}} 60 | Element Sizes ({{template "stats" .HashElementSizes}}): 61 | {{template "freq" .HashElementSizes}} 62 | ^2 Element Sizes:{{template "freq" power .HashElementSizes}} 63 | {{template "exampleValues" .HashValues}} 64 | Value Sizes ({{template "stats" .HashValueSizes}}): 65 | {{template "freq" .HashValueSizes}} 66 | ^2 Value Sizes:{{template "freq" power .HashValueSizes}}{{end}} 67 | 68 | {{ if .ListKeys }} 69 | --- Lists ({{summarize .ListSizes}}) --- 70 | {{template "exampleKeys" .ListKeys}} 71 | Sizes ({{template "stats" .ListSizes}}): 72 | {{template "freq" .ListSizes}} 73 | ^2 Sizes:{{template "freq" power .ListSizes}} 74 | {{template "exampleElements" .ListElements}} 75 | Element Sizes ({{template "stats" .ListElementSizes}}): 76 | {{template "freq" .ListElementSizes}} 77 | ^2 Element Sizes{{template "freq" power .ListElementSizes}} 78 | {{end}}{{end}} 79 | 80 | {{define "stats"}}{{ with stats . }}min: {{.Min}} max: {{.Max}} mean: {{fmtFloat .Mean}} std dev: {{fmtFloat .StdDev}}{{end}}{{end}} 81 | 82 | {{define "exampleKeys"}}Example Keys: 83 | {{range $k, $v := .}} {{$k}} 84 | {{end}}{{end}} 85 | 86 | {{define "exampleValues"}}Example Values: 87 | {{range $k, $v := .}} {{$k}} 88 | {{end}}{{end}} 89 | 90 | {{define "exampleElements"}}Example Elements: 91 | {{range $k, $v := .}} {{$k}} 92 | {{end}}{{end}} 93 | 94 | {{define "freq"}} 95 | {{ $ss := summarize . }}{{ range $s, $c := .}} {{$s}}: {{$c}} ({{percentage $c $ss }}) 96 | {{end}}{{end}} 97 | ` 98 | ) 99 | --------------------------------------------------------------------------------