See the component page for more information.
-
-
Save elsplatto/3ef95637f48aaf38cf39 to your computer and use it in GitHub Desktop.
qsuper-2 |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="CompilerConfiguration"> | |
<option name="DEFAULT_COMPILER" value="Javac" /> | |
<resourceExtensions /> | |
<wildcardResourcePatterns> | |
<entry name="!?*.java" /> | |
<entry name="!?*.form" /> | |
<entry name="!?*.class" /> | |
<entry name="!?*.groovy" /> | |
<entry name="!?*.scala" /> | |
<entry name="!?*.flex" /> | |
<entry name="!?*.kt" /> | |
<entry name="!?*.clj" /> | |
</wildcardResourcePatterns> | |
<annotationProcessing> | |
<profile default="true" name="Default" enabled="false"> | |
<processorPath useClasspath="true" /> | |
</profile> | |
</annotationProcessing> | |
</component> | |
</project> | |
<component name="CopyrightManager"> | |
<settings default="" /> | |
</component> |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="Encoding" useUTFGuessing="true" native2AsciiForPropertiesFiles="false" /> | |
</project> | |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="IdProvider" IDEtalkID="F770E3C0E64B20AC77D3D4918AD5EC48" /> | |
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_6" assert-keyword="true" jdk-15="true"> | |
<output url="file://$PROJECT_DIR$/out" /> | |
</component> | |
</project> | |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="ProjectModuleManager"> | |
<modules> | |
<module fileurl="file://$PROJECT_DIR$/qsuper-2.iml" filepath="$PROJECT_DIR$/qsuper-2.iml" /> | |
</modules> | |
</component> | |
</project> | |
<component name="DependencyValidationManager"> | |
<state> | |
<option name="SKIP_IMPORT_STATEMENTS" value="false" /> | |
</state> | |
</component> |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="Palette2"> | |
<group name="Swing"> | |
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" /> | |
</item> | |
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" /> | |
</item> | |
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" /> | |
</item> | |
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true"> | |
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" /> | |
</item> | |
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" /> | |
<initial-values> | |
<property name="text" value="Button" /> | |
</initial-values> | |
</item> | |
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> | |
<initial-values> | |
<property name="text" value="RadioButton" /> | |
</initial-values> | |
</item> | |
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" /> | |
<initial-values> | |
<property name="text" value="CheckBox" /> | |
</initial-values> | |
</item> | |
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" /> | |
<initial-values> | |
<property name="text" value="Label" /> | |
</initial-values> | |
</item> | |
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> | |
<preferred-size width="150" height="-1" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> | |
<preferred-size width="150" height="-1" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1"> | |
<preferred-size width="150" height="-1" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
<preferred-size width="150" height="50" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
<preferred-size width="150" height="50" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
<preferred-size width="150" height="50" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" /> | |
</item> | |
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
<preferred-size width="150" height="50" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3"> | |
<preferred-size width="150" height="50" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3"> | |
<preferred-size width="150" height="50" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> | |
<preferred-size width="200" height="200" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3"> | |
<preferred-size width="200" height="200" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> | |
</item> | |
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" /> | |
</item> | |
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" /> | |
</item> | |
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" /> | |
</item> | |
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1"> | |
<preferred-size width="-1" height="20" /> | |
</default-constraints> | |
</item> | |
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false"> | |
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" /> | |
</item> | |
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false"> | |
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" /> | |
</item> | |
</group> | |
</component> | |
</project> | |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="VcsDirectoryMappings"> | |
<mapping directory="" vcs="" /> | |
</component> | |
</project> | |
<?xml version="1.0" encoding="UTF-8"?> | |
<project version="4"> | |
<component name="ProjectTasksOptions"> | |
<TaskOptions isEnabled="true"> | |
<option name="arguments" value="--no-cache --update $FileName$:$FileNameWithoutExtension$.css" /> | |
<option name="checkSyntaxErrors" value="true" /> | |
<option name="description" value="Compiles .sass files into .css files" /> | |
<option name="exitCodeBehavior" value="ERROR" /> | |
<option name="fileExtension" value="sass" /> | |
<option name="immediateSync" value="true" /> | |
<option name="name" value="SASS" /> | |
<option name="output" value="../css/$FileNameWithoutExtension$.css" /> | |
<option name="outputFilters"> | |
<array /> | |
</option> | |
<option name="outputFromStdout" value="false" /> | |
<option name="passParentEnvs" value="true" /> | |
<option name="program" value="/usr/bin/sass" /> | |
<option name="scopeName" value="Project Files" /> | |
<option name="trackOnlyRoot" value="true" /> | |
<option name="workingDir" value="$FileDir$" /> | |
<envs /> | |
</TaskOptions> | |
</component> | |
</project> | |
{ | |
"name": "qsuper-2", | |
"version": "0.0.0", | |
"authors": [ | |
"Jason Taikato <[email protected]>" | |
], | |
"description": "QSuper Prototype using Polymer", | |
"main": "index.html", | |
"keywords": [ | |
"QSuper", | |
"Prototype", | |
"Polymer" | |
], | |
"license": "MIT", | |
"homepage": "index.html", | |
"ignore": [ | |
"**/.*", | |
"node_modules", | |
"bower_components", | |
"test", | |
"tests" | |
], | |
"dependencies": { | |
"polymer": "Polymer/polymer#~0.4.2", | |
"core-elements": "Polymer/core-elements#~0.4.2", | |
"paper-elements": "Polymer/paper-elements#~0.4.2", | |
"notification-elements": "PolymerLabs/notification-elements#~0.4.0", | |
"chart-elements": "robdodson/chart-elements#~0.1.1" | |
} | |
} |
{ | |
"name": "chart-elements", | |
"version": "0.1.1", | |
"keywords": "seed, polymer, web-components", | |
"main": "chart-elements.html", | |
"dependencies": { | |
"polymer": "Polymer/polymer#>=0.4.0 <1.0.0", | |
"chartjs": "~1.0.1-beta.4" | |
}, | |
"homepage": "https://github.com/robdodson/chart-elements", | |
"_release": "0.1.1", | |
"_resolution": { | |
"type": "version", | |
"tag": "v0.1.1", | |
"commit": "b212e2016fb29107cc6789a9e192b9871a276069" | |
}, | |
"_source": "git://github.com/robdodson/chart-elements.git", | |
"_target": "~0.1.1", | |
"_originalSource": "robdodson/chart-elements", | |
"_direct": true | |
} |
# EditorConfig helps developers define and maintain consistent | |
# coding styles between different editors and IDEs | |
# editorconfig.org | |
root = true | |
[*] | |
# Change these settings to your own preference | |
indent_style = space | |
indent_size = 2 | |
# We recommend you to keep these unchanged | |
end_of_line = lf | |
charset = utf-8 | |
trim_trailing_whitespace = true | |
insert_final_newline = true | |
[*.md] | |
trim_trailing_whitespace = false |
* text=auto |
node_modules | |
dist | |
.sass-cache | |
.tmp | |
.DS_Store | |
.bowerrc |
{ | |
"node": true, | |
"browser": true, | |
"esnext": true, | |
"bitwise": true, | |
"camelcase": true, | |
"curly": true, | |
"eqeqeq": true, | |
"immed": true, | |
"indent": 2, | |
"latedef": true, | |
"newcap": true, | |
"noarg": true, | |
"quotmark": "single", | |
"regexp": true, | |
"undef": true, | |
"unused": true, | |
"strict": true, | |
"trailing": true, | |
"smarttabs": true, | |
"jquery": true | |
} |
{ | |
"name": "chart-elements", | |
"version": "0.1.1", | |
"keywords": "seed, polymer, web-components", | |
"main": "chart-elements.html", | |
"dependencies": { | |
"polymer": "Polymer/polymer#>=0.4.0 <1.0.0", | |
"chartjs": "~1.0.1-beta.4" | |
} | |
} |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="chart-js-import.html"> | |
<!-- | |
A bar chart is a way of showing data as bars. | |
It is sometimes used to show trend data, and the comparison of multiple data sets side by side. | |
##### Example | |
<chart-bar width="400" height="200" | |
labels="['Jan','Feb','Mar','Apr','May','Jun','Jul']", | |
values="[[65,59,90,81,56,55,40], [28,48,40,19,96,27,100]]" | |
colors="['253,180,92','247,70,74','70,191,189','148,159,177','77,83,96']"> | |
</chart-bar> | |
@element chart-bar | |
@blurb A bar chart is a way of showing data as bars. | |
@status alpha | |
@homepage http://robdodson.github.io/chart-elements | |
--> | |
<polymer-element name="chart-bar" attributes="width height labels values colors"> | |
<template> | |
<canvas id="canvas" width="{{width}}" height="{{height}}"></canvas> | |
</template> | |
<script> | |
Polymer({ | |
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], | |
values: [ | |
[65,59,90,81,56,55,40], | |
[28,48,40,19,96,27,100] | |
], | |
colors: [ | |
"253,180,92", | |
"247,70,74", | |
"70,191,189", | |
"148,159,177", | |
"77,83,96" | |
], | |
observe: { | |
labels: 'updateChart', | |
colors: 'updateChart', | |
values: 'updateChart' | |
}, | |
updateChart: function() { | |
if (this.chart) { | |
this.chart.destroy(); | |
} | |
this.datasets = []; | |
this.values.forEach(function (val, i) { | |
this.datasets.push({ | |
fillColor: "rgba(" + this.colors[i] + ",0.5)", | |
strokeColor: "rgba(" + this.colors[i] + ",1)", | |
data: this.values[i] | |
}); | |
}, this); | |
this.data = { labels: this.labels, datasets: this.datasets }; | |
this.ctx = this.$.canvas.getContext('2d'); | |
this.chart = new Chart(this.ctx).Bar(this.data); | |
} | |
}); | |
</script> | |
</polymer-element> |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="chart-js-import.html"> | |
<!-- | |
Pie and doughnut charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows a the proportional value of each piece of data. | |
They are excellent at showing the relational proportions between data. | |
Pie and doughnut charts in are effectively the same class in Chart.js, but have one different default value - their percentageInnerCutout. This equates what percentage of the inner should be cut out. This defaults to 0 for pie charts, and 50 for doughnuts. | |
They are also registered under two aliases in the Chart core. Other than their different default value, and different alias, they are exactly the same. | |
##### Example | |
<chart-doughnut values="[30, 50, 100, 40, 120]"></chart-doughnut> | |
@element chart-doughnut | |
@blurb A chart for showing the relational proportions between data. | |
@status alpha | |
@homepage http://robdodson.github.io/chart-elements | |
--> | |
<polymer-element name="chart-doughnut" attributes="width height values colors"> | |
<template> | |
<canvas id="canvas" width="{{width}}" height="{{height}}"></canvas> | |
</template> | |
<script> | |
Polymer({ | |
values: [30, 50, 100, 40, 120], | |
colors: ["#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#4D5360"], | |
observe: { | |
colors: 'updateChart', | |
values: 'updateChart' | |
}, | |
updateChart: function () { | |
if(this.chart){ | |
this.chart.destroy(); | |
} | |
this.data = []; | |
this.values.forEach(function (val, i) { | |
this.data.push({ color: this.colors[i], value: val }); | |
}, this); | |
this.ctx = this.$.canvas.getContext('2d'); | |
this.chart = new Chart(this.ctx).Doughnut(this.data); | |
} | |
}); | |
</script> | |
</polymer-element> |
<link rel="import" href="chart-bar.html"> | |
<link rel="import" href="chart-doughnut.html"> | |
<link rel="import" href="chart-line.html"> | |
<link rel="import" href="chart-pie.html"> | |
<link rel="import" href="chart-polar-area.html"> | |
<link rel="import" href="chart-radar.html"> |
<script src="../chartjs/Chart.js"></script> |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="chart-js-import.html"> | |
<!-- | |
A line chart is a way of plotting data points on a line. | |
Often, it is used to show trend data, and the comparison of two data sets. | |
##### Example | |
<chart-line width="400" height="200" | |
labels="['Monday','Tuesday','Wednesday','thursday','Friday','Saturday','Sunday']" | |
values="[[10,14,20,25,13,9,40]]" | |
colors="['253,180,92','247,70,74','70,191,189','148,159,177','77,83,96']"> | |
</chart-line> | |
@element chart-line | |
@blurb A line chart is a way of plotting data points on a line. | |
@status alpha | |
@homepage http://robdodson.github.io/chart-elements | |
--> | |
<polymer-element name="chart-line" attributes="width height labels values colors"> | |
<template> | |
<canvas id="canvas" width="{{width}}" height="{{height}}"></canvas> | |
</template> | |
<script> | |
Polymer({ | |
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], | |
values: [ | |
[65,59,90,81,56,55,40], | |
[28,48,40,19,96,27,100] | |
], | |
colors: [ | |
"253,180,92", | |
"247,70,74", | |
"70,191,189", | |
"148,159,177", | |
"77,83,96" | |
], | |
observe: { | |
labels: 'updateChart', | |
colors: 'updateChart', | |
values: 'updateChart' | |
}, | |
updateChart:function(){ | |
if(this.chart){ | |
this.chart.destroy(); | |
} | |
this.datasets = []; | |
this.values.forEach(function (val, i) { | |
this.datasets.push({ | |
fillColor: "rgba(" + this.colors[i] + ",0.5)", | |
strokeColor: "rgba(" + this.colors[i] + ",1)", | |
pointColor: "rgba(" + this.colors[i] + ",1)", | |
pointStrokeColor: "#fff", | |
data: this.values[i] | |
}); | |
}, this); | |
this.data = { labels: this.labels, datasets: this.datasets }; | |
this.ctx = this.$.canvas.getContext('2d'); | |
this.chart = new Chart(this.ctx).Line(this.data); | |
} | |
}); | |
</script> | |
</polymer-element> |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="chart-js-import.html"> | |
<!-- | |
Pie and doughnut charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows a the proportional value of each piece of data. | |
They are excellent at showing the relational proportions between data. | |
Pie and doughnut charts in are effectively the same class in Chart.js, but have one different default value - their percentageInnerCutout. This equates what percentage of the inner should be cut out. This defaults to 0 for pie charts, and 50 for doughnuts. | |
They are also registered under two aliases in the Chart core. Other than their different default value, and different alias, they are exactly the same. | |
##### Example | |
<chart-pie values="[20, 30, 100]"></chart-pie> | |
@element chart-pie | |
@blurb A chart for showing the relational proportions between data. | |
@status alpha | |
@homepage http://robdodson.github.io/chart-elements | |
--> | |
<polymer-element name="chart-pie" attributes="width height values colors"> | |
<template> | |
<style> | |
:host { | |
display: inline-block; | |
} | |
</style> | |
<canvas id="canvas" width="{{width}}" height="{{height}}"></canvas> | |
</template> | |
<script> | |
Polymer({ | |
values: [30, 50, 100, 40, 120], | |
colors: ["#F7464A", "#46BFBD", "#FDB45C", "#949FB1", "#4D5360"], | |
observe: { | |
colors: 'updateChart', | |
values: 'updateChart' | |
}, | |
updateChart: function () { | |
if(this.chart){ | |
this.chart.destroy(); | |
} | |
this.data = []; | |
this.values.forEach(function (val, i) { | |
this.data.push({ color: this.colors[i], value: val }); | |
}, this); | |
this.ctx = this.$.canvas.getContext('2d'); | |
this.chart = new Chart(this.ctx).Pie(this.data); | |
} | |
}); | |
</script> | |
</polymer-element> |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="chart-js-import.html"> | |
<!-- | |
Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. | |
This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. | |
##### Example | |
<chart-polar-area values="[30, 90, 18, 58, 82]"></chart-polar-area> | |
@element chart-polar-area | |
@blurb Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. | |
@status alpha | |
@homepage http://robdodson.github.io/chart-elements | |
--> | |
<polymer-element name="chart-polar-area" attributes="width height values colors"> | |
<template> | |
<canvas id="canvas" width="{{width}}" height="{{height}}"></canvas> | |
</template> | |
<script> | |
Polymer("chart-polar-area", { | |
values: [30, 90, 18, 58, 82], | |
colors: ["#46BFBD", "#FDB45C", "#949FB1", "#4D5360", "#F7464A"], | |
observe: { | |
labels: 'updateChart', | |
colors: 'updateChart', | |
values: 'updateChart' | |
}, | |
updateChart: function () { | |
if(this.chart){ | |
this.chart.destroy(); | |
} | |
this.data = []; | |
this.values.forEach(function (val, i) { | |
this.data.push({ color: this.colors[i], value: val }); | |
}, this); | |
this.ctx = this.$.canvas.getContext('2d'); | |
this.chart = new Chart(this.ctx).PolarArea(this.data); | |
} | |
}); | |
</script> | |
</polymer-element> |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="chart-js-import.html"> | |
<!-- | |
A radar chart is a way of showing multiple data points and the variation between them. | |
They are often useful for comparing the points of two or more different data sets. | |
##### Example | |
<chart-radar labels="['Jan','Feb','Mar','Apr','May','Jun','Jul']" | |
values="[[65,59,90,81,56,55,40], [28,48,40,19,96,27,100]]", | |
colors= "['253,180,92','247,70,74','70,191,189','148,159,177','77,83,96']"> | |
</chart-radar> | |
@element chart-radar | |
@blurb A radar chart is a way of showing multiple data points and the variation between them. | |
@status alpha | |
@homepage http://robdodson.github.io/chart-elements | |
--> | |
<polymer-element name="chart-radar" attributes="width height labels values colors"> | |
<template> | |
<canvas id="canvas" width="{{width}}" height="{{height}}"></canvas> | |
</template> | |
<script> | |
Polymer({ | |
labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"], | |
values: [ | |
[65,59,90,81,56,55,40], | |
[28,48,40,19,96,27,100] | |
], | |
colors: [ | |
"253,180,92", | |
"247,70,74", | |
"70,191,189", | |
"148,159,177", | |
"77,83,96" | |
], | |
observe: { | |
colors: 'updateChart', | |
values: 'updateChart', | |
labels: 'updateChart' | |
}, | |
updateChart: function () { | |
if(this.chart){ | |
this.chart.destroy(); | |
} | |
this.datasets = []; | |
this.values.forEach(function (val, i) { | |
this.datasets.push({ | |
fillColor: "rgba(" + this.colors[i] + ",0.5)", | |
strokeColor: "rgba(" + this.colors[i] + ",1)", | |
pointColor: "rgba(" + this.colors[i] + ",1)", | |
pointStrokeColor: "#fff", | |
data: this.values[i] | |
}); | |
}, this); | |
this.data = { labels: this.labels, datasets: this.datasets }; | |
this.ctx = this.$.canvas.getContext('2d'); | |
this.chart = new Chart(this.ctx).Radar(this.data); | |
} | |
}); | |
</script> | |
</polymer-element> |
<!doctype html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> | |
<title>chart-elements Demo</title> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="chart-elements.html"> | |
</head> | |
<body unresolved> | |
<chart-pie | |
width="200" | |
height="200" | |
values="[20, 30, 100]"> | |
</chart-pie> | |
<chart-doughnut | |
width="200" | |
height="200" | |
values="[30, 50, 100, 40, 120]"> | |
</chart-doughnut> | |
<chart-polar-area | |
width="200" | |
height="200" | |
values="[30, 90, 18, 58, 82]"> | |
</chart-polar-area> | |
<chart-radar labels="['Jan','Feb','Mar','Apr','May','Jun','Jul']" | |
values="[[65,59,90,81,56,55,40], [28,48,40,19,96,27,100]]", | |
colors= "['253,180,92','247,70,74','70,191,189','148,159,177','77,83,96']"> | |
</chart-radar> | |
<br> | |
<chart-line | |
width="400" | |
height="200" | |
labels="['Monday','Tuesday','Wednesday','thursday','Friday','Saturday','Sunday']" | |
values="[[10,14,20,25,13,9,40]]" | |
colors="['253,180,92','247,70,74','70,191,189','148,159,177','77,83,96']"> | |
</chart-line> | |
<chart-bar | |
width="400" | |
height="200" | |
labels="['Jan','Feb','Mar','Apr','May','Jun','Jul']", | |
values="[[65,59,90,81,56,55,40], [28,48,40,19,96,27,100]]" | |
colors="['253,180,92','247,70,74','70,191,189','148,159,177','77,83,96']"> | |
</chart-bar> | |
</body> | |
</html> |
<!doctype html> | |
<html> | |
<head> | |
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="../polymer/polymer.html"> | |
<link rel="import" href="../core-component-page/core-component-page.html"> | |
</head> | |
<body unresolved> | |
<core-component-page | |
moduleName="chart-elements" | |
sources='[ | |
"chart-bar.html", | |
"chart-doughnut.html", | |
"chart-line.html", | |
"chart-pie.html", | |
"chart-polar-area.html", | |
"chart-radar.html" | |
]'> | |
</core-component-page> | |
</body> | |
</html> |
{ | |
"name": "Chart.js", | |
"version": "1.0.1-beta.4", | |
"description": "Simple HTML5 Charts using the canvas element", | |
"homepage": "https://github.com/nnnick/Chart.js", | |
"author": "nnnick", | |
"main": [ | |
"Chart.min.js" | |
], | |
"dependencies": {}, | |
"_release": "1.0.1-beta.4", | |
"_resolution": { | |
"type": "version", | |
"tag": "v1.0.1-beta.4", | |
"commit": "8b840ce56be51b61dfc2d4e40e56304c880c541b" | |
}, | |
"_source": "git://github.com/nnnick/Chart.js.git", | |
"_target": "~1.0.1-beta.4", | |
"_originalSource": "chartjs" | |
} |
.DS_Store | |
node_modules/* | |
custom/* | |
docs/index.md |
{ | |
"name": "Chart.js", | |
"version": "1.0.1-beta.4", | |
"description": "Simple HTML5 Charts using the canvas element", | |
"homepage": "https://github.com/nnnick/Chart.js", | |
"author": "nnnick", | |
"main": [ | |
"Chart.min.js" | |
], | |
"dependencies": {} | |
} |
/*! | |
* Chart.js | |
* http://chartjs.org/ | |
* Version: 1.0.1-beta.4 | |
* | |
* Copyright 2014 Nick Downie | |
* Released under the MIT license | |
* https://github.com/nnnick/Chart.js/blob/master/LICENSE.md | |
*/ | |
(function(){ | |
"use strict"; | |
//Declare root variable - window in the browser, global on the server | |
var root = this, | |
previous = root.Chart; | |
//Occupy the global variable of Chart, and create a simple base class | |
var Chart = function(context){ | |
var chart = this; | |
this.canvas = context.canvas; | |
this.ctx = context; | |
//Variables global to the chart | |
var width = this.width = context.canvas.width; | |
var height = this.height = context.canvas.height; | |
this.aspectRatio = this.width / this.height; | |
//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. | |
helpers.retinaScale(this); | |
return this; | |
}; | |
//Globally expose the defaults to allow for user updating/changing | |
Chart.defaults = { | |
global: { | |
// Boolean - Whether to animate the chart | |
animation: true, | |
// Number - Number of animation steps | |
animationSteps: 60, | |
// String - Animation easing effect | |
animationEasing: "easeOutQuart", | |
// Boolean - If we should show the scale at all | |
showScale: true, | |
// Boolean - If we want to override with a hard coded scale | |
scaleOverride: false, | |
// ** Required if scaleOverride is true ** | |
// Number - The number of steps in a hard coded scale | |
scaleSteps: null, | |
// Number - The value jump in the hard coded scale | |
scaleStepWidth: null, | |
// Number - The scale starting value | |
scaleStartValue: null, | |
// String - Colour of the scale line | |
scaleLineColor: "rgba(0,0,0,.1)", | |
// Number - Pixel width of the scale line | |
scaleLineWidth: 1, | |
// Boolean - Whether to show labels on the scale | |
scaleShowLabels: true, | |
// Interpolated JS string - can access value | |
scaleLabel: "<%=value%>", | |
// Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there | |
scaleIntegersOnly: true, | |
// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value | |
scaleBeginAtZero: false, | |
// String - Scale label font declaration for the scale label | |
scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", | |
// Number - Scale label font size in pixels | |
scaleFontSize: 12, | |
// String - Scale label font weight style | |
scaleFontStyle: "normal", | |
// String - Scale label font colour | |
scaleFontColor: "#666", | |
// Boolean - whether or not the chart should be responsive and resize when the browser does. | |
responsive: false, | |
// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container | |
maintainAspectRatio: true, | |
// Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove | |
showTooltips: true, | |
// Array - Array of string names to attach tooltip events | |
tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], | |
// String - Tooltip background colour | |
tooltipFillColor: "rgba(0,0,0,0.8)", | |
// String - Tooltip label font declaration for the scale label | |
tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", | |
// Number - Tooltip label font size in pixels | |
tooltipFontSize: 14, | |
// String - Tooltip font weight style | |
tooltipFontStyle: "normal", | |
// String - Tooltip label font colour | |
tooltipFontColor: "#fff", | |
// String - Tooltip title font declaration for the scale label | |
tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", | |
// Number - Tooltip title font size in pixels | |
tooltipTitleFontSize: 14, | |
// String - Tooltip title font weight style | |
tooltipTitleFontStyle: "bold", | |
// String - Tooltip title font colour | |
tooltipTitleFontColor: "#fff", | |
// Number - pixel width of padding around tooltip text | |
tooltipYPadding: 6, | |
// Number - pixel width of padding around tooltip text | |
tooltipXPadding: 6, | |
// Number - Size of the caret on the tooltip | |
tooltipCaretSize: 8, | |
// Number - Pixel radius of the tooltip border | |
tooltipCornerRadius: 6, | |
// Number - Pixel offset from point x to tooltip edge | |
tooltipXOffset: 10, | |
// String - Template string for single tooltips | |
tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", | |
// String - Template string for single tooltips | |
multiTooltipTemplate: "<%= value %>", | |
// String - Colour behind the legend colour block | |
multiTooltipKeyBackground: '#fff', | |
// Function - Will fire on animation progression. | |
onAnimationProgress: function(){}, | |
// Function - Will fire on animation completion. | |
onAnimationComplete: function(){} | |
} | |
}; | |
//Create a dictionary of chart types, to allow for extension of existing types | |
Chart.types = {}; | |
//Global Chart helpers object for utility methods and classes | |
var helpers = Chart.helpers = {}; | |
//-- Basic js utility methods | |
var each = helpers.each = function(loopable,callback,self){ | |
var additionalArgs = Array.prototype.slice.call(arguments, 3); | |
// Check to see if null or undefined firstly. | |
if (loopable){ | |
if (loopable.length === +loopable.length){ | |
var i; | |
for (i=0; i<loopable.length; i++){ | |
callback.apply(self,[loopable[i], i].concat(additionalArgs)); | |
} | |
} | |
else{ | |
for (var item in loopable){ | |
callback.apply(self,[loopable[item],item].concat(additionalArgs)); | |
} | |
} | |
} | |
}, | |
clone = helpers.clone = function(obj){ | |
var objClone = {}; | |
each(obj,function(value,key){ | |
if (obj.hasOwnProperty(key)) objClone[key] = value; | |
}); | |
return objClone; | |
}, | |
extend = helpers.extend = function(base){ | |
each(Array.prototype.slice.call(arguments,1), function(extensionObject) { | |
each(extensionObject,function(value,key){ | |
if (extensionObject.hasOwnProperty(key)) base[key] = value; | |
}); | |
}); | |
return base; | |
}, | |
merge = helpers.merge = function(base,master){ | |
//Merge properties in left object over to a shallow clone of object right. | |
var args = Array.prototype.slice.call(arguments,0); | |
args.unshift({}); | |
return extend.apply(null, args); | |
}, | |
indexOf = helpers.indexOf = function(arrayToSearch, item){ | |
if (Array.prototype.indexOf) { | |
return arrayToSearch.indexOf(item); | |
} | |
else{ | |
for (var i = 0; i < arrayToSearch.length; i++) { | |
if (arrayToSearch[i] === item) return i; | |
} | |
return -1; | |
} | |
}, | |
where = helpers.where = function(collection, filterCallback){ | |
var filtered = []; | |
helpers.each(collection, function(item){ | |
if (filterCallback(item)){ | |
filtered.push(item); | |
} | |
}); | |
return filtered; | |
}, | |
findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){ | |
// Default to start of the array | |
if (!startIndex){ | |
startIndex = -1; | |
} | |
for (var i = startIndex + 1; i < arrayToSearch.length; i++) { | |
var currentItem = arrayToSearch[i]; | |
if (filterCallback(currentItem)){ | |
return currentItem; | |
} | |
}; | |
}, | |
findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){ | |
// Default to end of the array | |
if (!startIndex){ | |
startIndex = arrayToSearch.length; | |
} | |
for (var i = startIndex - 1; i >= 0; i--) { | |
var currentItem = arrayToSearch[i]; | |
if (filterCallback(currentItem)){ | |
return currentItem; | |
} | |
}; | |
}, | |
inherits = helpers.inherits = function(extensions){ | |
//Basic javascript inheritance based on the model created in Backbone.js | |
var parent = this; | |
var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; | |
var Surrogate = function(){ this.constructor = ChartElement;}; | |
Surrogate.prototype = parent.prototype; | |
ChartElement.prototype = new Surrogate(); | |
ChartElement.extend = inherits; | |
if (extensions) extend(ChartElement.prototype, extensions); | |
ChartElement.__super__ = parent.prototype; | |
return ChartElement; | |
}, | |
noop = helpers.noop = function(){}, | |
uid = helpers.uid = (function(){ | |
var id=0; | |
return function(){ | |
return "chart-" + id++; | |
}; | |
})(), | |
warn = helpers.warn = function(str){ | |
//Method for warning of errors | |
if (window.console && typeof window.console.warn == "function") console.warn(str); | |
}, | |
amd = helpers.amd = (typeof root.define == 'function' && root.define.amd), | |
//-- Math methods | |
isNumber = helpers.isNumber = function(n){ | |
return !isNaN(parseFloat(n)) && isFinite(n); | |
}, | |
max = helpers.max = function(array){ | |
return Math.max.apply( Math, array ); | |
}, | |
min = helpers.min = function(array){ | |
return Math.min.apply( Math, array ); | |
}, | |
cap = helpers.cap = function(valueToCap,maxValue,minValue){ | |
if(isNumber(maxValue)) { | |
if( valueToCap > maxValue ) { | |
return maxValue; | |
} | |
} | |
else if(isNumber(minValue)){ | |
if ( valueToCap < minValue ){ | |
return minValue; | |
} | |
} | |
return valueToCap; | |
}, | |
getDecimalPlaces = helpers.getDecimalPlaces = function(num){ | |
if (num%1!==0 && isNumber(num)){ | |
return num.toString().split(".")[1].length; | |
} | |
else { | |
return 0; | |
} | |
}, | |
toRadians = helpers.radians = function(degrees){ | |
return degrees * (Math.PI/180); | |
}, | |
// Gets the angle from vertical upright to the point about a centre. | |
getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ | |
var distanceFromXCenter = anglePoint.x - centrePoint.x, | |
distanceFromYCenter = anglePoint.y - centrePoint.y, | |
radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); | |
var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); | |
//If the segment is in the top left quadrant, we need to add another rotation to the angle | |
if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ | |
angle += Math.PI*2; | |
} | |
return { | |
angle: angle, | |
distance: radialDistanceFromCenter | |
}; | |
}, | |
aliasPixel = helpers.aliasPixel = function(pixelWidth){ | |
return (pixelWidth % 2 === 0) ? 0 : 0.5; | |
}, | |
splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ | |
//Props to Rob Spencer at scaled innovation for his post on splining between points | |
//http://scaledinnovation.com/analytics/splines/aboutSplines.html | |
var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), | |
d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), | |
fa=t*d01/(d01+d12),// scaling factor for triangle Ta | |
fb=t*d12/(d01+d12); | |
return { | |
inner : { | |
x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), | |
y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) | |
}, | |
outer : { | |
x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), | |
y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) | |
} | |
}; | |
}, | |
calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ | |
return Math.floor(Math.log(val) / Math.LN10); | |
}, | |
calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ | |
//Set a minimum step of two - a point at the top of the graph, and a point at the base | |
var minSteps = 2, | |
maxSteps = Math.floor(drawingSize/(textSize * 1.5)), | |
skipFitting = (minSteps >= maxSteps); | |
var maxValue = max(valuesArray), | |
minValue = min(valuesArray); | |
// We need some degree of seperation here to calculate the scales if all the values are the same | |
// Adding/minusing 0.5 will give us a range of 1. | |
if (maxValue === minValue){ | |
maxValue += 0.5; | |
// So we don't end up with a graph with a negative start value if we've said always start from zero | |
if (minValue >= 0.5 && !startFromZero){ | |
minValue -= 0.5; | |
} | |
else{ | |
// Make up a whole number above the values | |
maxValue += 0.5; | |
} | |
} | |
var valueRange = Math.abs(maxValue - minValue), | |
rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), | |
graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), | |
graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), | |
graphRange = graphMax - graphMin, | |
stepValue = Math.pow(10, rangeOrderOfMagnitude), | |
numberOfSteps = Math.round(graphRange / stepValue); | |
//If we have more space on the graph we'll use it to give more definition to the data | |
while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { | |
if(numberOfSteps > maxSteps){ | |
stepValue *=2; | |
numberOfSteps = Math.round(graphRange/stepValue); | |
// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. | |
if (numberOfSteps % 1 !== 0){ | |
skipFitting = true; | |
} | |
} | |
//We can fit in double the amount of scale points on the scale | |
else{ | |
//If user has declared ints only, and the step value isn't a decimal | |
if (integersOnly && rangeOrderOfMagnitude >= 0){ | |
//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float | |
if(stepValue/2 % 1 === 0){ | |
stepValue /=2; | |
numberOfSteps = Math.round(graphRange/stepValue); | |
} | |
//If it would make it a float break out of the loop | |
else{ | |
break; | |
} | |
} | |
//If the scale doesn't have to be an int, make the scale more granular anyway. | |
else{ | |
stepValue /=2; | |
numberOfSteps = Math.round(graphRange/stepValue); | |
} | |
} | |
} | |
if (skipFitting){ | |
numberOfSteps = minSteps; | |
stepValue = graphRange / numberOfSteps; | |
} | |
return { | |
steps : numberOfSteps, | |
stepValue : stepValue, | |
min : graphMin, | |
max : graphMin + (numberOfSteps * stepValue) | |
}; | |
}, | |
/* jshint ignore:start */ | |
// Blows up jshint errors based on the new Function constructor | |
//Templating methods | |
//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ | |
template = helpers.template = function(templateString, valuesObject){ | |
// If templateString is function rather than string-template - call the function for valuesObject | |
if(templateString instanceof Function){ | |
return templateString(valuesObject); | |
} | |
var cache = {}; | |
function tmpl(str, data){ | |
// Figure out if we're getting a template, or if we need to | |
// load the template - and be sure to cache the result. | |
var fn = !/\W/.test(str) ? | |
cache[str] = cache[str] : | |
// Generate a reusable function that will serve as a template | |
// generator (and which will be cached). | |
new Function("obj", | |
"var p=[],print=function(){p.push.apply(p,arguments);};" + | |
// Introduce the data as local variables using with(){} | |
"with(obj){p.push('" + | |
// Convert the template into pure JavaScript | |
str | |
.replace(/[\r\t\n]/g, " ") | |
.split("<%").join("\t") | |
.replace(/((^|%>)[^\t]*)'/g, "$1\r") | |
.replace(/\t=(.*?)%>/g, "',$1,'") | |
.split("\t").join("');") | |
.split("%>").join("p.push('") | |
.split("\r").join("\\'") + | |
"');}return p.join('');" | |
); | |
// Provide some basic currying to the user | |
return data ? fn( data ) : fn; | |
} | |
return tmpl(templateString,valuesObject); | |
}, | |
/* jshint ignore:end */ | |
generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ | |
var labelsArray = new Array(numberOfSteps); | |
if (labelTemplateString){ | |
each(labelsArray,function(val,index){ | |
labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); | |
}); | |
} | |
return labelsArray; | |
}, | |
//--Animation methods | |
//Easing functions adapted from Robert Penner's easing equations | |
//http://www.robertpenner.com/easing/ | |
easingEffects = helpers.easingEffects = { | |
linear: function (t) { | |
return t; | |
}, | |
easeInQuad: function (t) { | |
return t * t; | |
}, | |
easeOutQuad: function (t) { | |
return -1 * t * (t - 2); | |
}, | |
easeInOutQuad: function (t) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; | |
return -1 / 2 * ((--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) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; | |
return 1 / 2 * ((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) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; | |
return -1 / 2 * ((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) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; | |
return 1 / 2 * ((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 -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); | |
}, | |
easeInExpo: function (t) { | |
return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); | |
}, | |
easeOutExpo: function (t) { | |
return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); | |
}, | |
easeInOutExpo: function (t) { | |
if (t === 0) return 0; | |
if (t === 1) return 1; | |
if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); | |
return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); | |
}, | |
easeInCirc: function (t) { | |
if (t >= 1) return t; | |
return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); | |
}, | |
easeOutCirc: function (t) { | |
return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); | |
}, | |
easeInOutCirc: function (t) { | |
if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); | |
return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); | |
}, | |
easeInElastic: function (t) { | |
var s = 1.70158; | |
var p = 0; | |
var a = 1; | |
if (t === 0) return 0; | |
if ((t /= 1) == 1) return 1; | |
if (!p) p = 1 * 0.3; | |
if (a < Math.abs(1)) { | |
a = 1; | |
s = p / 4; | |
} else s = p / (2 * Math.PI) * Math.asin(1 / a); | |
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); | |
}, | |
easeOutElastic: function (t) { | |
var s = 1.70158; | |
var p = 0; | |
var a = 1; | |
if (t === 0) return 0; | |
if ((t /= 1) == 1) return 1; | |
if (!p) p = 1 * 0.3; | |
if (a < Math.abs(1)) { | |
a = 1; | |
s = p / 4; | |
} else s = p / (2 * Math.PI) * Math.asin(1 / a); | |
return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; | |
}, | |
easeInOutElastic: function (t) { | |
var s = 1.70158; | |
var p = 0; | |
var a = 1; | |
if (t === 0) return 0; | |
if ((t /= 1 / 2) == 2) return 1; | |
if (!p) p = 1 * (0.3 * 1.5); | |
if (a < Math.abs(1)) { | |
a = 1; | |
s = p / 4; | |
} else s = p / (2 * Math.PI) * Math.asin(1 / a); | |
if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); | |
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; | |
}, | |
easeInBack: function (t) { | |
var s = 1.70158; | |
return 1 * (t /= 1) * t * ((s + 1) * t - s); | |
}, | |
easeOutBack: function (t) { | |
var s = 1.70158; | |
return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); | |
}, | |
easeInOutBack: function (t) { | |
var s = 1.70158; | |
if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); | |
return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); | |
}, | |
easeInBounce: function (t) { | |
return 1 - easingEffects.easeOutBounce(1 - t); | |
}, | |
easeOutBounce: function (t) { | |
if ((t /= 1) < (1 / 2.75)) { | |
return 1 * (7.5625 * t * t); | |
} else if (t < (2 / 2.75)) { | |
return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); | |
} else if (t < (2.5 / 2.75)) { | |
return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); | |
} else { | |
return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); | |
} | |
}, | |
easeInOutBounce: function (t) { | |
if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; | |
return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; | |
} | |
}, | |
//Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
requestAnimFrame = helpers.requestAnimFrame = (function(){ | |
return window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.oRequestAnimationFrame || | |
window.msRequestAnimationFrame || | |
function(callback) { | |
return window.setTimeout(callback, 1000 / 60); | |
}; | |
})(), | |
cancelAnimFrame = helpers.cancelAnimFrame = (function(){ | |
return window.cancelAnimationFrame || | |
window.webkitCancelAnimationFrame || | |
window.mozCancelAnimationFrame || | |
window.oCancelAnimationFrame || | |
window.msCancelAnimationFrame || | |
function(callback) { | |
return window.clearTimeout(callback, 1000 / 60); | |
}; | |
})(), | |
animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ | |
var currentStep = 0, | |
easingFunction = easingEffects[easingString] || easingEffects.linear; | |
var animationFrame = function(){ | |
currentStep++; | |
var stepDecimal = currentStep/totalSteps; | |
var easeDecimal = easingFunction(stepDecimal); | |
callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); | |
onProgress.call(chartInstance,easeDecimal,stepDecimal); | |
if (currentStep < totalSteps){ | |
chartInstance.animationFrame = requestAnimFrame(animationFrame); | |
} else{ | |
onComplete.apply(chartInstance); | |
} | |
}; | |
requestAnimFrame(animationFrame); | |
}, | |
//-- DOM methods | |
getRelativePosition = helpers.getRelativePosition = function(evt){ | |
var mouseX, mouseY; | |
var e = evt.originalEvent || evt, | |
canvas = evt.currentTarget || evt.srcElement, | |
boundingRect = canvas.getBoundingClientRect(); | |
if (e.touches){ | |
mouseX = e.touches[0].clientX - boundingRect.left; | |
mouseY = e.touches[0].clientY - boundingRect.top; | |
} | |
else{ | |
mouseX = e.clientX - boundingRect.left; | |
mouseY = e.clientY - boundingRect.top; | |
} | |
return { | |
x : mouseX, | |
y : mouseY | |
}; | |
}, | |
addEvent = helpers.addEvent = function(node,eventType,method){ | |
if (node.addEventListener){ | |
node.addEventListener(eventType,method); | |
} else if (node.attachEvent){ | |
node.attachEvent("on"+eventType, method); | |
} else { | |
node["on"+eventType] = method; | |
} | |
}, | |
removeEvent = helpers.removeEvent = function(node, eventType, handler){ | |
if (node.removeEventListener){ | |
node.removeEventListener(eventType, handler, false); | |
} else if (node.detachEvent){ | |
node.detachEvent("on"+eventType,handler); | |
} else{ | |
node["on" + eventType] = noop; | |
} | |
}, | |
bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ | |
// Create the events object if it's not already present | |
if (!chartInstance.events) chartInstance.events = {}; | |
each(arrayOfEvents,function(eventName){ | |
chartInstance.events[eventName] = function(){ | |
handler.apply(chartInstance, arguments); | |
}; | |
addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); | |
}); | |
}, | |
unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { | |
each(arrayOfEvents, function(handler,eventName){ | |
removeEvent(chartInstance.chart.canvas, eventName, handler); | |
}); | |
}, | |
getMaximumWidth = helpers.getMaximumWidth = function(domNode){ | |
var container = domNode.parentNode; | |
// TODO = check cross browser stuff with this. | |
return container.clientWidth; | |
}, | |
getMaximumHeight = helpers.getMaximumHeight = function(domNode){ | |
var container = domNode.parentNode; | |
// TODO = check cross browser stuff with this. | |
return container.clientHeight; | |
}, | |
getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support | |
retinaScale = helpers.retinaScale = function(chart){ | |
var ctx = chart.ctx, | |
width = chart.canvas.width, | |
height = chart.canvas.height; | |
if (window.devicePixelRatio) { | |
ctx.canvas.style.width = width + "px"; | |
ctx.canvas.style.height = height + "px"; | |
ctx.canvas.height = height * window.devicePixelRatio; | |
ctx.canvas.width = width * window.devicePixelRatio; | |
ctx.scale(window.devicePixelRatio, window.devicePixelRatio); | |
} | |
}, | |
//-- Canvas methods | |
clear = helpers.clear = function(chart){ | |
chart.ctx.clearRect(0,0,chart.width,chart.height); | |
}, | |
fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ | |
return fontStyle + " " + pixelSize+"px " + fontFamily; | |
}, | |
longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ | |
ctx.font = font; | |
var longest = 0; | |
each(arrayOfStrings,function(string){ | |
var textWidth = ctx.measureText(string).width; | |
longest = (textWidth > longest) ? textWidth : longest; | |
}); | |
return longest; | |
}, | |
drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ | |
ctx.beginPath(); | |
ctx.moveTo(x + radius, y); | |
ctx.lineTo(x + width - radius, y); | |
ctx.quadraticCurveTo(x + width, y, x + width, y + radius); | |
ctx.lineTo(x + width, y + height - radius); | |
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); | |
ctx.lineTo(x + radius, y + height); | |
ctx.quadraticCurveTo(x, y + height, x, y + height - radius); | |
ctx.lineTo(x, y + radius); | |
ctx.quadraticCurveTo(x, y, x + radius, y); | |
ctx.closePath(); | |
}; | |
//Store a reference to each instance - allowing us to globally resize chart instances on window resize. | |
//Destroy method on the chart will remove the instance of the chart from this reference. | |
Chart.instances = {}; | |
Chart.Type = function(data,options,chart){ | |
this.options = options; | |
this.chart = chart; | |
this.id = uid(); | |
//Add the chart instance to the global namespace | |
Chart.instances[this.id] = this; | |
// Initialize is always called when a chart type is created | |
// By default it is a no op, but it should be extended | |
if (options.responsive){ | |
this.resize(); | |
} | |
this.initialize.call(this,data); | |
}; | |
//Core methods that'll be a part of every chart type | |
extend(Chart.Type.prototype,{ | |
initialize : function(){return this;}, | |
clear : function(){ | |
clear(this.chart); | |
return this; | |
}, | |
stop : function(){ | |
// Stops any current animation loop occuring | |
helpers.cancelAnimFrame.call(root, this.animationFrame); | |
return this; | |
}, | |
resize : function(callback){ | |
this.stop(); | |
var canvas = this.chart.canvas, | |
newWidth = getMaximumWidth(this.chart.canvas), | |
newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); | |
canvas.width = this.chart.width = newWidth; | |
canvas.height = this.chart.height = newHeight; | |
retinaScale(this.chart); | |
if (typeof callback === "function"){ | |
callback.apply(this, Array.prototype.slice.call(arguments, 1)); | |
} | |
return this; | |
}, | |
reflow : noop, | |
render : function(reflow){ | |
if (reflow){ | |
this.reflow(); | |
} | |
if (this.options.animation && !reflow){ | |
helpers.animationLoop( | |
this.draw, | |
this.options.animationSteps, | |
this.options.animationEasing, | |
this.options.onAnimationProgress, | |
this.options.onAnimationComplete, | |
this | |
); | |
} | |
else{ | |
this.draw(); | |
this.options.onAnimationComplete.call(this); | |
} | |
return this; | |
}, | |
generateLegend : function(){ | |
return template(this.options.legendTemplate,this); | |
}, | |
destroy : function(){ | |
this.clear(); | |
unbindEvents(this, this.events); | |
delete Chart.instances[this.id]; | |
}, | |
showTooltip : function(ChartElements, forceRedraw){ | |
// Only redraw the chart if we've actually changed what we're hovering on. | |
if (typeof this.activeElements === 'undefined') this.activeElements = []; | |
var isChanged = (function(Elements){ | |
var changed = false; | |
if (Elements.length !== this.activeElements.length){ | |
changed = true; | |
return changed; | |
} | |
each(Elements, function(element, index){ | |
if (element !== this.activeElements[index]){ | |
changed = true; | |
} | |
}, this); | |
return changed; | |
}).call(this, ChartElements); | |
if (!isChanged && !forceRedraw){ | |
return; | |
} | |
else{ | |
this.activeElements = ChartElements; | |
} | |
this.draw(); | |
if (ChartElements.length > 0){ | |
// If we have multiple datasets, show a MultiTooltip for all of the data points at that index | |
if (this.datasets && this.datasets.length > 1) { | |
var dataArray, | |
dataIndex; | |
for (var i = this.datasets.length - 1; i >= 0; i--) { | |
dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; | |
dataIndex = indexOf(dataArray, ChartElements[0]); | |
if (dataIndex !== -1){ | |
break; | |
} | |
} | |
var tooltipLabels = [], | |
tooltipColors = [], | |
medianPosition = (function(index) { | |
// Get all the points at that particular index | |
var Elements = [], | |
dataCollection, | |
xPositions = [], | |
yPositions = [], | |
xMax, | |
yMax, | |
xMin, | |
yMin; | |
helpers.each(this.datasets, function(dataset){ | |
dataCollection = dataset.points || dataset.bars || dataset.segments; | |
if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ | |
Elements.push(dataCollection[dataIndex]); | |
} | |
}); | |
helpers.each(Elements, function(element) { | |
xPositions.push(element.x); | |
yPositions.push(element.y); | |
//Include any colour information about the element | |
tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); | |
tooltipColors.push({ | |
fill: element._saved.fillColor || element.fillColor, | |
stroke: element._saved.strokeColor || element.strokeColor | |
}); | |
}, this); | |
yMin = min(yPositions); | |
yMax = max(yPositions); | |
xMin = min(xPositions); | |
xMax = max(xPositions); | |
return { | |
x: (xMin > this.chart.width/2) ? xMin : xMax, | |
y: (yMin + yMax)/2 | |
}; | |
}).call(this, dataIndex); | |
new Chart.MultiTooltip({ | |
x: medianPosition.x, | |
y: medianPosition.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: tooltipLabels, | |
legendColors: tooltipColors, | |
legendColorBackground : this.options.multiTooltipKeyBackground, | |
title: ChartElements[0].label, | |
chart: this.chart, | |
ctx: this.chart.ctx | |
}).draw(); | |
} else { | |
each(ChartElements, function(Element) { | |
var tooltipPosition = Element.tooltipPosition(); | |
new Chart.Tooltip({ | |
x: Math.round(tooltipPosition.x), | |
y: Math.round(tooltipPosition.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: template(this.options.tooltipTemplate, Element), | |
chart: this.chart | |
}).draw(); | |
}, this); | |
} | |
} | |
return this; | |
}, | |
toBase64Image : function(){ | |
return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); | |
} | |
}); | |
Chart.Type.extend = function(extensions){ | |
var parent = this; | |
var ChartType = function(){ | |
return parent.apply(this,arguments); | |
}; | |
//Copy the prototype object of the this class | |
ChartType.prototype = clone(parent.prototype); | |
//Now overwrite some of the properties in the base class with the new extensions | |
extend(ChartType.prototype, extensions); | |
ChartType.extend = Chart.Type.extend; | |
if (extensions.name || parent.prototype.name){ | |
var chartName = extensions.name || parent.prototype.name; | |
//Assign any potential default values of the new chart type | |
//If none are defined, we'll use a clone of the chart type this is being extended from. | |
//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart | |
//doesn't define some defaults of their own. | |
var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; | |
Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); | |
Chart.types[chartName] = ChartType; | |
//Register this new chart type in the Chart prototype | |
Chart.prototype[chartName] = function(data,options){ | |
var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); | |
return new ChartType(data,config,this); | |
}; | |
} else{ | |
warn("Name not provided for this chart, so it hasn't been registered"); | |
} | |
return parent; | |
}; | |
Chart.Element = function(configuration){ | |
extend(this,configuration); | |
this.initialize.apply(this,arguments); | |
this.save(); | |
}; | |
extend(Chart.Element.prototype,{ | |
initialize : function(){}, | |
restore : function(props){ | |
if (!props){ | |
extend(this,this._saved); | |
} else { | |
each(props,function(key){ | |
this[key] = this._saved[key]; | |
},this); | |
} | |
return this; | |
}, | |
save : function(){ | |
this._saved = clone(this); | |
delete this._saved._saved; | |
return this; | |
}, | |
update : function(newProps){ | |
each(newProps,function(value,key){ | |
this._saved[key] = this[key]; | |
this[key] = value; | |
},this); | |
return this; | |
}, | |
transition : function(props,ease){ | |
each(props,function(value,key){ | |
this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; | |
},this); | |
return this; | |
}, | |
tooltipPosition : function(){ | |
return { | |
x : this.x, | |
y : this.y | |
}; | |
}, | |
hasValue: function(){ | |
return isNumber(this.value); | |
} | |
}); | |
Chart.Element.extend = inherits; | |
Chart.Point = Chart.Element.extend({ | |
display: true, | |
inRange: function(chartX,chartY){ | |
var hitDetectionRange = this.hitDetectionRadius + this.radius; | |
return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); | |
}, | |
draw : function(){ | |
if (this.display){ | |
var ctx = this.ctx; | |
ctx.beginPath(); | |
ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); | |
ctx.closePath(); | |
ctx.strokeStyle = this.strokeColor; | |
ctx.lineWidth = this.strokeWidth; | |
ctx.fillStyle = this.fillColor; | |
ctx.fill(); | |
ctx.stroke(); | |
} | |
//Quick debug for bezier curve splining | |
//Highlights control points and the line between them. | |
//Handy for dev - stripped in the min version. | |
// ctx.save(); | |
// ctx.fillStyle = "black"; | |
// ctx.strokeStyle = "black" | |
// ctx.beginPath(); | |
// ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); | |
// ctx.fill(); | |
// ctx.beginPath(); | |
// ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); | |
// ctx.fill(); | |
// ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); | |
// ctx.lineTo(this.x, this.y); | |
// ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); | |
// ctx.stroke(); | |
// ctx.restore(); | |
} | |
}); | |
Chart.Arc = Chart.Element.extend({ | |
inRange : function(chartX,chartY){ | |
var pointRelativePosition = helpers.getAngleFromPoint(this, { | |
x: chartX, | |
y: chartY | |
}); | |
//Check if within the range of the open/close angle | |
var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), | |
withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); | |
return (betweenAngles && withinRadius); | |
//Ensure within the outside of the arc centre, but inside arc outer | |
}, | |
tooltipPosition : function(){ | |
var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), | |
rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; | |
return { | |
x : this.x + (Math.cos(centreAngle) * rangeFromCentre), | |
y : this.y + (Math.sin(centreAngle) * rangeFromCentre) | |
}; | |
}, | |
draw : function(animationPercent){ | |
var easingDecimal = animationPercent || 1; | |
var ctx = this.ctx; | |
ctx.beginPath(); | |
ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); | |
ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); | |
ctx.closePath(); | |
ctx.strokeStyle = this.strokeColor; | |
ctx.lineWidth = this.strokeWidth; | |
ctx.fillStyle = this.fillColor; | |
ctx.fill(); | |
ctx.lineJoin = 'bevel'; | |
if (this.showStroke){ | |
ctx.stroke(); | |
} | |
} | |
}); | |
Chart.Rectangle = Chart.Element.extend({ | |
draw : function(){ | |
var ctx = this.ctx, | |
halfWidth = this.width/2, | |
leftX = this.x - halfWidth, | |
rightX = this.x + halfWidth, | |
top = this.base - (this.base - this.y), | |
halfStroke = this.strokeWidth / 2; | |
// Canvas doesn't allow us to stroke inside the width so we can | |
// adjust the sizes to fit if we're setting a stroke on the line | |
if (this.showStroke){ | |
leftX += halfStroke; | |
rightX -= halfStroke; | |
top += halfStroke; | |
} | |
ctx.beginPath(); | |
ctx.fillStyle = this.fillColor; | |
ctx.strokeStyle = this.strokeColor; | |
ctx.lineWidth = this.strokeWidth; | |
// It'd be nice to keep this class totally generic to any rectangle | |
// and simply specify which border to miss out. | |
ctx.moveTo(leftX, this.base); | |
ctx.lineTo(leftX, top); | |
ctx.lineTo(rightX, top); | |
ctx.lineTo(rightX, this.base); | |
ctx.fill(); | |
if (this.showStroke){ | |
ctx.stroke(); | |
} | |
}, | |
height : function(){ | |
return this.base - this.y; | |
}, | |
inRange : function(chartX,chartY){ | |
return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); | |
} | |
}); | |
Chart.Tooltip = Chart.Element.extend({ | |
draw : function(){ | |
var ctx = this.chart.ctx; | |
ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); | |
this.xAlign = "center"; | |
this.yAlign = "above"; | |
//Distance between the actual element.y position and the start of the tooltip caret | |
var caretPadding = 2; | |
var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, | |
tooltipRectHeight = this.fontSize + 2*this.yPadding, | |
tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; | |
if (this.x + tooltipWidth/2 >this.chart.width){ | |
this.xAlign = "left"; | |
} else if (this.x - tooltipWidth/2 < 0){ | |
this.xAlign = "right"; | |
} | |
if (this.y - tooltipHeight < 0){ | |
this.yAlign = "below"; | |
} | |
var tooltipX = this.x - tooltipWidth/2, | |
tooltipY = this.y - tooltipHeight; | |
ctx.fillStyle = this.fillColor; | |
switch(this.yAlign) | |
{ | |
case "above": | |
//Draw a caret above the x/y | |
ctx.beginPath(); | |
ctx.moveTo(this.x,this.y - caretPadding); | |
ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); | |
ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
case "below": | |
tooltipY = this.y + caretPadding + this.caretHeight; | |
//Draw a caret below the x/y | |
ctx.beginPath(); | |
ctx.moveTo(this.x, this.y + caretPadding); | |
ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); | |
ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
} | |
switch(this.xAlign) | |
{ | |
case "left": | |
tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); | |
break; | |
case "right": | |
tooltipX = this.x - (this.cornerRadius + this.caretHeight); | |
break; | |
} | |
drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); | |
ctx.fill(); | |
ctx.fillStyle = this.textColor; | |
ctx.textAlign = "center"; | |
ctx.textBaseline = "middle"; | |
ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); | |
} | |
}); | |
Chart.MultiTooltip = Chart.Element.extend({ | |
initialize : function(){ | |
this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); | |
this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); | |
this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; | |
this.ctx.font = this.titleFont; | |
var titleWidth = this.ctx.measureText(this.title).width, | |
//Label has a legend square as well so account for this. | |
labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, | |
longestTextWidth = max([labelWidth,titleWidth]); | |
this.width = longestTextWidth + (this.xPadding*2); | |
var halfHeight = this.height/2; | |
//Check to ensure the height will fit on the canvas | |
//The three is to buffer form the very | |
if (this.y - halfHeight < 0 ){ | |
this.y = halfHeight; | |
} else if (this.y + halfHeight > this.chart.height){ | |
this.y = this.chart.height - halfHeight; | |
} | |
//Decide whether to align left or right based on position on canvas | |
if (this.x > this.chart.width/2){ | |
this.x -= this.xOffset + this.width; | |
} else { | |
this.x += this.xOffset; | |
} | |
}, | |
getLineHeight : function(index){ | |
var baseLineHeight = this.y - (this.height/2) + this.yPadding, | |
afterTitleIndex = index-1; | |
//If the index is zero, we're getting the title | |
if (index === 0){ | |
return baseLineHeight + this.titleFontSize/2; | |
} else{ | |
return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; | |
} | |
}, | |
draw : function(){ | |
drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); | |
var ctx = this.ctx; | |
ctx.fillStyle = this.fillColor; | |
ctx.fill(); | |
ctx.closePath(); | |
ctx.textAlign = "left"; | |
ctx.textBaseline = "middle"; | |
ctx.fillStyle = this.titleTextColor; | |
ctx.font = this.titleFont; | |
ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); | |
ctx.font = this.font; | |
helpers.each(this.labels,function(label,index){ | |
ctx.fillStyle = this.textColor; | |
ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); | |
//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) | |
//ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); | |
//Instead we'll make a white filled block to put the legendColour palette over. | |
ctx.fillStyle = this.legendColorBackground; | |
ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); | |
ctx.fillStyle = this.legendColors[index].fill; | |
ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); | |
},this); | |
} | |
}); | |
Chart.Scale = Chart.Element.extend({ | |
initialize : function(){ | |
this.fit(); | |
}, | |
buildYLabels : function(){ | |
this.yLabels = []; | |
var stepDecimalPlaces = getDecimalPlaces(this.stepValue); | |
for (var i=0; i<=this.steps; i++){ | |
this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); | |
} | |
this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; | |
}, | |
addXLabel : function(label){ | |
this.xLabels.push(label); | |
this.valuesCount++; | |
this.fit(); | |
}, | |
removeXLabel : function(){ | |
this.xLabels.shift(); | |
this.valuesCount--; | |
this.fit(); | |
}, | |
// Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use | |
fit: function(){ | |
// First we need the width of the yLabels, assuming the xLabels aren't rotated | |
// To do that we need the base line at the top and base of the chart, assuming there is no x label rotation | |
this.startPoint = (this.display) ? this.fontSize : 0; | |
this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels | |
// Apply padding settings to the start and end point. | |
this.startPoint += this.padding; | |
this.endPoint -= this.padding; | |
// Cache the starting height, so can determine if we need to recalculate the scale yAxis | |
var cachedHeight = this.endPoint - this.startPoint, | |
cachedYLabelWidth; | |
// Build the current yLabels so we have an idea of what size they'll be to start | |
/* | |
* This sets what is returned from calculateScaleRange as static properties of this class: | |
* | |
this.steps; | |
this.stepValue; | |
this.min; | |
this.max; | |
* | |
*/ | |
this.calculateYRange(cachedHeight); | |
// With these properties set we can now build the array of yLabels | |
// and also the width of the largest yLabel | |
this.buildYLabels(); | |
this.calculateXLabelRotation(); | |
while((cachedHeight > this.endPoint - this.startPoint)){ | |
cachedHeight = this.endPoint - this.startPoint; | |
cachedYLabelWidth = this.yLabelWidth; | |
this.calculateYRange(cachedHeight); | |
this.buildYLabels(); | |
// Only go through the xLabel loop again if the yLabel width has changed | |
if (cachedYLabelWidth < this.yLabelWidth){ | |
this.calculateXLabelRotation(); | |
} | |
} | |
}, | |
calculateXLabelRotation : function(){ | |
//Get the width of each grid by calculating the difference | |
//between x offsets between 0 and 1. | |
this.ctx.font = this.font; | |
var firstWidth = this.ctx.measureText(this.xLabels[0]).width, | |
lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, | |
firstRotated, | |
lastRotated; | |
this.xScalePaddingRight = lastWidth/2 + 3; | |
this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; | |
this.xLabelRotation = 0; | |
if (this.display){ | |
var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), | |
cosRotation, | |
firstRotatedWidth; | |
this.xLabelWidth = originalLabelWidth; | |
//Allow 3 pixels x2 padding either side for label readability | |
var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; | |
//Max label rotate should be 90 - also act as a loop counter | |
while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ | |
cosRotation = Math.cos(toRadians(this.xLabelRotation)); | |
firstRotated = cosRotation * firstWidth; | |
lastRotated = cosRotation * lastWidth; | |
// We're right aligning the text now. | |
if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ | |
this.xScalePaddingLeft = firstRotated + this.fontSize / 2; | |
} | |
this.xScalePaddingRight = this.fontSize/2; | |
this.xLabelRotation++; | |
this.xLabelWidth = cosRotation * originalLabelWidth; | |
} | |
if (this.xLabelRotation > 0){ | |
this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; | |
} | |
} | |
else{ | |
this.xLabelWidth = 0; | |
this.xScalePaddingRight = this.padding; | |
this.xScalePaddingLeft = this.padding; | |
} | |
}, | |
// Needs to be overidden in each Chart type | |
// Otherwise we need to pass all the data into the scale class | |
calculateYRange: noop, | |
drawingArea: function(){ | |
return this.startPoint - this.endPoint; | |
}, | |
calculateY : function(value){ | |
var scalingFactor = this.drawingArea() / (this.min - this.max); | |
return this.endPoint - (scalingFactor * (value - this.min)); | |
}, | |
calculateX : function(index){ | |
var isRotated = (this.xLabelRotation > 0), | |
// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, | |
innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), | |
valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), | |
valueOffset = (valueWidth * index) + this.xScalePaddingLeft; | |
if (this.offsetGridLines){ | |
valueOffset += (valueWidth/2); | |
} | |
return Math.round(valueOffset); | |
}, | |
update : function(newProps){ | |
helpers.extend(this, newProps); | |
this.fit(); | |
}, | |
draw : function(){ | |
var ctx = this.ctx, | |
yLabelGap = (this.endPoint - this.startPoint) / this.steps, | |
xStart = Math.round(this.xScalePaddingLeft); | |
if (this.display){ | |
ctx.fillStyle = this.textColor; | |
ctx.font = this.font; | |
each(this.yLabels,function(labelString,index){ | |
var yLabelCenter = this.endPoint - (yLabelGap * index), | |
linePositionY = Math.round(yLabelCenter); | |
ctx.textAlign = "right"; | |
ctx.textBaseline = "middle"; | |
if (this.showLabels){ | |
ctx.fillText(labelString,xStart - 10,yLabelCenter); | |
} | |
ctx.beginPath(); | |
if (index > 0){ | |
// This is a grid line in the centre, so drop that | |
ctx.lineWidth = this.gridLineWidth; | |
ctx.strokeStyle = this.gridLineColor; | |
} else { | |
// This is the first line on the scale | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
} | |
linePositionY += helpers.aliasPixel(ctx.lineWidth); | |
ctx.moveTo(xStart, linePositionY); | |
ctx.lineTo(this.width, linePositionY); | |
ctx.stroke(); | |
ctx.closePath(); | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
ctx.beginPath(); | |
ctx.moveTo(xStart - 5, linePositionY); | |
ctx.lineTo(xStart, linePositionY); | |
ctx.stroke(); | |
ctx.closePath(); | |
},this); | |
each(this.xLabels,function(label,index){ | |
var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), | |
// Check to see if line/bar here and decide where to place the line | |
linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), | |
isRotated = (this.xLabelRotation > 0); | |
ctx.beginPath(); | |
if (index > 0){ | |
// This is a grid line in the centre, so drop that | |
ctx.lineWidth = this.gridLineWidth; | |
ctx.strokeStyle = this.gridLineColor; | |
} else { | |
// This is the first line on the scale | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
} | |
ctx.moveTo(linePos,this.endPoint); | |
ctx.lineTo(linePos,this.startPoint - 3); | |
ctx.stroke(); | |
ctx.closePath(); | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
// Small lines at the bottom of the base grid line | |
ctx.beginPath(); | |
ctx.moveTo(linePos,this.endPoint); | |
ctx.lineTo(linePos,this.endPoint + 5); | |
ctx.stroke(); | |
ctx.closePath(); | |
ctx.save(); | |
ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); | |
ctx.rotate(toRadians(this.xLabelRotation)*-1); | |
ctx.font = this.font; | |
ctx.textAlign = (isRotated) ? "right" : "center"; | |
ctx.textBaseline = (isRotated) ? "middle" : "top"; | |
ctx.fillText(label, 0, 0); | |
ctx.restore(); | |
},this); | |
} | |
} | |
}); | |
Chart.RadialScale = Chart.Element.extend({ | |
initialize: function(){ | |
this.size = min([this.height, this.width]); | |
this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); | |
}, | |
calculateCenterOffset: function(value){ | |
// Take into account half font size + the yPadding of the top value | |
var scalingFactor = this.drawingArea / (this.max - this.min); | |
return (value - this.min) * scalingFactor; | |
}, | |
update : function(){ | |
if (!this.lineArc){ | |
this.setScaleSize(); | |
} else { | |
this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); | |
} | |
this.buildYLabels(); | |
}, | |
buildYLabels: function(){ | |
this.yLabels = []; | |
var stepDecimalPlaces = getDecimalPlaces(this.stepValue); | |
for (var i=0; i<=this.steps; i++){ | |
this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); | |
} | |
}, | |
getCircumference : function(){ | |
return ((Math.PI*2) / this.valuesCount); | |
}, | |
setScaleSize: function(){ | |
/* | |
* Right, this is really confusing and there is a lot of maths going on here | |
* The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 | |
* | |
* Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif | |
* | |
* Solution: | |
* | |
* We assume the radius of the polygon is half the size of the canvas at first | |
* at each index we check if the text overlaps. | |
* | |
* Where it does, we store that angle and that index. | |
* | |
* After finding the largest index and angle we calculate how much we need to remove | |
* from the shape radius to move the point inwards by that x. | |
* | |
* We average the left and right distances to get the maximum shape radius that can fit in the box | |
* along with labels. | |
* | |
* Once we have that, we can find the centre point for the chart, by taking the x text protrusion | |
* on each side, removing that from the size, halving it and adding the left x protrusion width. | |
* | |
* This will mean we have a shape fitted to the canvas, as large as it can be with the labels | |
* and position it in the most space efficient manner | |
* | |
* https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif | |
*/ | |
// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. | |
// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points | |
var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), | |
pointPosition, | |
i, | |
textWidth, | |
halfTextWidth, | |
furthestRight = this.width, | |
furthestRightIndex, | |
furthestRightAngle, | |
furthestLeft = 0, | |
furthestLeftIndex, | |
furthestLeftAngle, | |
xProtrusionLeft, | |
xProtrusionRight, | |
radiusReductionRight, | |
radiusReductionLeft, | |
maxWidthRadius; | |
this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); | |
for (i=0;i<this.valuesCount;i++){ | |
// 5px to space the text slightly out - similar to what we do in the draw function. | |
pointPosition = this.getPointPosition(i, largestPossibleRadius); | |
textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5; | |
if (i === 0 || i === this.valuesCount/2){ | |
// If we're at index zero, or exactly the middle, we're at exactly the top/bottom | |
// of the radar chart, so text will be aligned centrally, so we'll half it and compare | |
// w/left and right text sizes | |
halfTextWidth = textWidth/2; | |
if (pointPosition.x + halfTextWidth > furthestRight) { | |
furthestRight = pointPosition.x + halfTextWidth; | |
furthestRightIndex = i; | |
} | |
if (pointPosition.x - halfTextWidth < furthestLeft) { | |
furthestLeft = pointPosition.x - halfTextWidth; | |
furthestLeftIndex = i; | |
} | |
} | |
else if (i < this.valuesCount/2) { | |
// Less than half the values means we'll left align the text | |
if (pointPosition.x + textWidth > furthestRight) { | |
furthestRight = pointPosition.x + textWidth; | |
furthestRightIndex = i; | |
} | |
} | |
else if (i > this.valuesCount/2){ | |
// More than half the values means we'll right align the text | |
if (pointPosition.x - textWidth < furthestLeft) { | |
furthestLeft = pointPosition.x - textWidth; | |
furthestLeftIndex = i; | |
} | |
} | |
} | |
xProtrusionLeft = furthestLeft; | |
xProtrusionRight = Math.ceil(furthestRight - this.width); | |
furthestRightAngle = this.getIndexAngle(furthestRightIndex); | |
furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); | |
radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); | |
radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); | |
// Ensure we actually need to reduce the size of the chart | |
radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; | |
radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; | |
this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; | |
//this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) | |
this.setCenterPoint(radiusReductionLeft, radiusReductionRight); | |
}, | |
setCenterPoint: function(leftMovement, rightMovement){ | |
var maxRight = this.width - rightMovement - this.drawingArea, | |
maxLeft = leftMovement + this.drawingArea; | |
this.xCenter = (maxLeft + maxRight)/2; | |
// Always vertically in the centre as the text height doesn't change | |
this.yCenter = (this.height/2); | |
}, | |
getIndexAngle : function(index){ | |
var angleMultiplier = (Math.PI * 2) / this.valuesCount; | |
// Start from the top instead of right, so remove a quarter of the circle | |
return index * angleMultiplier - (Math.PI/2); | |
}, | |
getPointPosition : function(index, distanceFromCenter){ | |
var thisAngle = this.getIndexAngle(index); | |
return { | |
x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, | |
y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter | |
}; | |
}, | |
draw: function(){ | |
if (this.display){ | |
var ctx = this.ctx; | |
each(this.yLabels, function(label, index){ | |
// Don't draw a centre value | |
if (index > 0){ | |
var yCenterOffset = index * (this.drawingArea/this.steps), | |
yHeight = this.yCenter - yCenterOffset, | |
pointPosition; | |
// Draw circular lines around the scale | |
if (this.lineWidth > 0){ | |
ctx.strokeStyle = this.lineColor; | |
ctx.lineWidth = this.lineWidth; | |
if(this.lineArc){ | |
ctx.beginPath(); | |
ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); | |
ctx.closePath(); | |
ctx.stroke(); | |
} else{ | |
ctx.beginPath(); | |
for (var i=0;i<this.valuesCount;i++) | |
{ | |
pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue))); | |
if (i === 0){ | |
ctx.moveTo(pointPosition.x, pointPosition.y); | |
} else { | |
ctx.lineTo(pointPosition.x, pointPosition.y); | |
} | |
} | |
ctx.closePath(); | |
ctx.stroke(); | |
} | |
} | |
if(this.showLabels){ | |
ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); | |
if (this.showLabelBackdrop){ | |
var labelWidth = ctx.measureText(label).width; | |
ctx.fillStyle = this.backdropColor; | |
ctx.fillRect( | |
this.xCenter - labelWidth/2 - this.backdropPaddingX, | |
yHeight - this.fontSize/2 - this.backdropPaddingY, | |
labelWidth + this.backdropPaddingX*2, | |
this.fontSize + this.backdropPaddingY*2 | |
); | |
} | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = "middle"; | |
ctx.fillStyle = this.fontColor; | |
ctx.fillText(label, this.xCenter, yHeight); | |
} | |
} | |
}, this); | |
if (!this.lineArc){ | |
ctx.lineWidth = this.angleLineWidth; | |
ctx.strokeStyle = this.angleLineColor; | |
for (var i = this.valuesCount - 1; i >= 0; i--) { | |
if (this.angleLineWidth > 0){ | |
var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); | |
ctx.beginPath(); | |
ctx.moveTo(this.xCenter, this.yCenter); | |
ctx.lineTo(outerPosition.x, outerPosition.y); | |
ctx.stroke(); | |
ctx.closePath(); | |
} | |
// Extra 3px out for some label spacing | |
var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); | |
ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); | |
ctx.fillStyle = this.pointLabelFontColor; | |
var labelsCount = this.labels.length, | |
halfLabelsCount = this.labels.length/2, | |
quarterLabelsCount = halfLabelsCount/2, | |
upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), | |
exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); | |
if (i === 0){ | |
ctx.textAlign = 'center'; | |
} else if(i === halfLabelsCount){ | |
ctx.textAlign = 'center'; | |
} else if (i < halfLabelsCount){ | |
ctx.textAlign = 'left'; | |
} else { | |
ctx.textAlign = 'right'; | |
} | |
// Set the correct text baseline based on outer positioning | |
if (exactQuarter){ | |
ctx.textBaseline = 'middle'; | |
} else if (upperHalf){ | |
ctx.textBaseline = 'bottom'; | |
} else { | |
ctx.textBaseline = 'top'; | |
} | |
ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); | |
} | |
} | |
} | |
} | |
}); | |
// Attach global event to resize each chart instance when the browser resizes | |
helpers.addEvent(window, "resize", (function(){ | |
// Basic debounce of resize function so it doesn't hurt performance when resizing browser. | |
var timeout; | |
return function(){ | |
clearTimeout(timeout); | |
timeout = setTimeout(function(){ | |
each(Chart.instances,function(instance){ | |
// If the responsive flag is set in the chart instance config | |
// Cascade the resize event down to the chart. | |
if (instance.options.responsive){ | |
instance.resize(instance.render, true); | |
} | |
}); | |
}, 50); | |
}; | |
})()); | |
if (amd) { | |
define(function(){ | |
return Chart; | |
}); | |
} else if (typeof module === 'object' && module.exports) { | |
module.exports = Chart; | |
} | |
root.Chart = Chart; | |
Chart.noConflict = function(){ | |
root.Chart = previous; | |
return Chart; | |
}; | |
}).call(this); | |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value | |
scaleBeginAtZero : true, | |
//Boolean - Whether grid lines are shown across the chart | |
scaleShowGridLines : true, | |
//String - Colour of the grid lines | |
scaleGridLineColor : "rgba(0,0,0,.05)", | |
//Number - Width of the grid lines | |
scaleGridLineWidth : 1, | |
//Boolean - If there is a stroke on each bar | |
barShowStroke : true, | |
//Number - Pixel width of the bar stroke | |
barStrokeWidth : 2, | |
//Number - Spacing between each of the X value sets | |
barValueSpacing : 5, | |
//Number - Spacing between data sets within X values | |
barDatasetSpacing : 1, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
name: "Bar", | |
defaults : defaultConfig, | |
initialize: function(data){ | |
//Expose options as a scope variable here so we can access it in the ScaleClass | |
var options = this.options; | |
this.ScaleClass = Chart.Scale.extend({ | |
offsetGridLines : true, | |
calculateBarX : function(datasetCount, datasetIndex, barIndex){ | |
//Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar | |
var xWidth = this.calculateBaseWidth(), | |
xAbsolute = this.calculateX(barIndex) - (xWidth/2), | |
barWidth = this.calculateBarWidth(datasetCount); | |
return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; | |
}, | |
calculateBaseWidth : function(){ | |
return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); | |
}, | |
calculateBarWidth : function(datasetCount){ | |
//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset | |
var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); | |
return (baseWidth / datasetCount); | |
} | |
}); | |
this.datasets = []; | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; | |
this.eachBars(function(bar){ | |
bar.restore(['fillColor', 'strokeColor']); | |
}); | |
helpers.each(activeBars, function(activeBar){ | |
activeBar.fillColor = activeBar.highlightFill; | |
activeBar.strokeColor = activeBar.highlightStroke; | |
}); | |
this.showTooltip(activeBars); | |
}); | |
} | |
//Declare the extension of the default point, to cater for the options passed in to the constructor | |
this.BarClass = Chart.Rectangle.extend({ | |
strokeWidth : this.options.barStrokeWidth, | |
showStroke : this.options.barShowStroke, | |
ctx : this.chart.ctx | |
}); | |
//Iterate through each of the datasets, and build this into a property of the chart | |
helpers.each(data.datasets,function(dataset,datasetIndex){ | |
var datasetObject = { | |
label : dataset.label || null, | |
fillColor : dataset.fillColor, | |
strokeColor : dataset.strokeColor, | |
bars : [] | |
}; | |
this.datasets.push(datasetObject); | |
helpers.each(dataset.data,function(dataPoint,index){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
datasetObject.bars.push(new this.BarClass({ | |
value : dataPoint, | |
label : data.labels[index], | |
datasetLabel: dataset.label, | |
strokeColor : dataset.strokeColor, | |
fillColor : dataset.fillColor, | |
highlightFill : dataset.highlightFill || dataset.fillColor, | |
highlightStroke : dataset.highlightStroke || dataset.strokeColor | |
})); | |
},this); | |
},this); | |
this.buildScale(data.labels); | |
this.BarClass.prototype.base = this.scale.endPoint; | |
this.eachBars(function(bar, index, datasetIndex){ | |
helpers.extend(bar, { | |
width : this.scale.calculateBarWidth(this.datasets.length), | |
x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), | |
y: this.scale.endPoint | |
}); | |
bar.save(); | |
}, this); | |
this.render(); | |
}, | |
update : function(){ | |
this.scale.update(); | |
// Reset any highlight colours before updating. | |
helpers.each(this.activeElements, function(activeElement){ | |
activeElement.restore(['fillColor', 'strokeColor']); | |
}); | |
this.eachBars(function(bar){ | |
bar.save(); | |
}); | |
this.render(); | |
}, | |
eachBars : function(callback){ | |
helpers.each(this.datasets,function(dataset, datasetIndex){ | |
helpers.each(dataset.bars, callback, this, datasetIndex); | |
},this); | |
}, | |
getBarsAtEvent : function(e){ | |
var barsArray = [], | |
eventPosition = helpers.getRelativePosition(e), | |
datasetIterator = function(dataset){ | |
barsArray.push(dataset.bars[barIndex]); | |
}, | |
barIndex; | |
for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { | |
for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { | |
if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ | |
helpers.each(this.datasets, datasetIterator); | |
return barsArray; | |
} | |
} | |
} | |
return barsArray; | |
}, | |
buildScale : function(labels){ | |
var self = this; | |
var dataTotal = function(){ | |
var values = []; | |
self.eachBars(function(bar){ | |
values.push(bar.value); | |
}); | |
return values; | |
}; | |
var scaleOptions = { | |
templateString : this.options.scaleLabel, | |
height : this.chart.height, | |
width : this.chart.width, | |
ctx : this.chart.ctx, | |
textColor : this.options.scaleFontColor, | |
fontSize : this.options.scaleFontSize, | |
fontStyle : this.options.scaleFontStyle, | |
fontFamily : this.options.scaleFontFamily, | |
valuesCount : labels.length, | |
beginAtZero : this.options.scaleBeginAtZero, | |
integersOnly : this.options.scaleIntegersOnly, | |
calculateYRange: function(currentHeight){ | |
var updatedRanges = helpers.calculateScaleRange( | |
dataTotal(), | |
currentHeight, | |
this.fontSize, | |
this.beginAtZero, | |
this.integersOnly | |
); | |
helpers.extend(this, updatedRanges); | |
}, | |
xLabels : labels, | |
font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), | |
lineWidth : this.options.scaleLineWidth, | |
lineColor : this.options.scaleLineColor, | |
gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, | |
gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", | |
padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, | |
showLabels : this.options.scaleShowLabels, | |
display : this.options.showScale | |
}; | |
if (this.options.scaleOverride){ | |
helpers.extend(scaleOptions, { | |
calculateYRange: helpers.noop, | |
steps: this.options.scaleSteps, | |
stepValue: this.options.scaleStepWidth, | |
min: this.options.scaleStartValue, | |
max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) | |
}); | |
} | |
this.scale = new this.ScaleClass(scaleOptions); | |
}, | |
addData : function(valuesArray,label){ | |
//Map the values array for each of the datasets | |
helpers.each(valuesArray,function(value,datasetIndex){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
this.datasets[datasetIndex].bars.push(new this.BarClass({ | |
value : value, | |
label : label, | |
x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), | |
y: this.scale.endPoint, | |
width : this.scale.calculateBarWidth(this.datasets.length), | |
base : this.scale.endPoint, | |
strokeColor : this.datasets[datasetIndex].strokeColor, | |
fillColor : this.datasets[datasetIndex].fillColor | |
})); | |
},this); | |
this.scale.addXLabel(label); | |
//Then re-render the chart. | |
this.update(); | |
}, | |
removeData : function(){ | |
this.scale.removeXLabel(); | |
//Then re-render the chart. | |
helpers.each(this.datasets,function(dataset){ | |
dataset.bars.shift(); | |
},this); | |
this.update(); | |
}, | |
reflow : function(){ | |
helpers.extend(this.BarClass.prototype,{ | |
y: this.scale.endPoint, | |
base : this.scale.endPoint | |
}); | |
var newScaleProps = helpers.extend({ | |
height : this.chart.height, | |
width : this.chart.width | |
}); | |
this.scale.update(newScaleProps); | |
}, | |
draw : function(ease){ | |
var easingDecimal = ease || 1; | |
this.clear(); | |
var ctx = this.chart.ctx; | |
this.scale.draw(easingDecimal); | |
//Draw all the bars for each dataset | |
helpers.each(this.datasets,function(dataset,datasetIndex){ | |
helpers.each(dataset.bars,function(bar,index){ | |
if (bar.hasValue()){ | |
bar.base = this.scale.endPoint; | |
//Transition then draw | |
bar.transition({ | |
x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), | |
y : this.scale.calculateY(bar.value), | |
width : this.scale.calculateBarWidth(this.datasets.length) | |
}, easingDecimal).draw(); | |
} | |
},this); | |
},this); | |
} | |
}); | |
}).call(this); | |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
//Cache a local reference to Chart.helpers | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
//Boolean - Whether we should show a stroke on each segment | |
segmentShowStroke : true, | |
//String - The colour of each segment stroke | |
segmentStrokeColor : "#fff", | |
//Number - The width of each segment stroke | |
segmentStrokeWidth : 2, | |
//The percentage of the chart that we cut out of the middle. | |
percentageInnerCutout : 50, | |
//Number - Amount of animation steps | |
animationSteps : 100, | |
//String - Animation easing effect | |
animationEasing : "easeOutBounce", | |
//Boolean - Whether we animate the rotation of the Doughnut | |
animateRotate : true, | |
//Boolean - Whether we animate scaling the Doughnut from the centre | |
animateScale : false, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
//Passing in a name registers this chart in the Chart namespace | |
name: "Doughnut", | |
//Providing a defaults will also register the deafults in the chart namespace | |
defaults : defaultConfig, | |
//Initialize is fired when the chart is initialized - Data is passed in as a parameter | |
//Config is automatically merged by the core of Chart.js, and is available at this.options | |
initialize: function(data){ | |
//Declare segments as a static property to prevent inheriting across the Chart type prototype | |
this.segments = []; | |
this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; | |
this.SegmentArc = Chart.Arc.extend({ | |
ctx : this.chart.ctx, | |
x : this.chart.width/2, | |
y : this.chart.height/2 | |
}); | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; | |
helpers.each(this.segments,function(segment){ | |
segment.restore(["fillColor"]); | |
}); | |
helpers.each(activeSegments,function(activeSegment){ | |
activeSegment.fillColor = activeSegment.highlightColor; | |
}); | |
this.showTooltip(activeSegments); | |
}); | |
} | |
this.calculateTotal(data); | |
helpers.each(data,function(datapoint, index){ | |
this.addData(datapoint, index, true); | |
},this); | |
this.render(); | |
}, | |
getSegmentsAtEvent : function(e){ | |
var segmentsArray = []; | |
var location = helpers.getRelativePosition(e); | |
helpers.each(this.segments,function(segment){ | |
if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); | |
},this); | |
return segmentsArray; | |
}, | |
addData : function(segment, atIndex, silent){ | |
var index = atIndex || this.segments.length; | |
this.segments.splice(index, 0, new this.SegmentArc({ | |
value : segment.value, | |
outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, | |
innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, | |
fillColor : segment.color, | |
highlightColor : segment.highlight || segment.color, | |
showStroke : this.options.segmentShowStroke, | |
strokeWidth : this.options.segmentStrokeWidth, | |
strokeColor : this.options.segmentStrokeColor, | |
startAngle : Math.PI * 1.5, | |
circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), | |
label : segment.label | |
})); | |
if (!silent){ | |
this.reflow(); | |
this.update(); | |
} | |
}, | |
calculateCircumference : function(value){ | |
return (Math.PI*2)*(value / this.total); | |
}, | |
calculateTotal : function(data){ | |
this.total = 0; | |
helpers.each(data,function(segment){ | |
this.total += segment.value; | |
},this); | |
}, | |
update : function(){ | |
this.calculateTotal(this.segments); | |
// Reset any highlight colours before updating. | |
helpers.each(this.activeElements, function(activeElement){ | |
activeElement.restore(['fillColor']); | |
}); | |
helpers.each(this.segments,function(segment){ | |
segment.save(); | |
}); | |
this.render(); | |
}, | |
removeData: function(atIndex){ | |
var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; | |
this.segments.splice(indexToDelete, 1); | |
this.reflow(); | |
this.update(); | |
}, | |
reflow : function(){ | |
helpers.extend(this.SegmentArc.prototype,{ | |
x : this.chart.width/2, | |
y : this.chart.height/2 | |
}); | |
this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; | |
helpers.each(this.segments, function(segment){ | |
segment.update({ | |
outerRadius : this.outerRadius, | |
innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout | |
}); | |
}, this); | |
}, | |
draw : function(easeDecimal){ | |
var animDecimal = (easeDecimal) ? easeDecimal : 1; | |
this.clear(); | |
helpers.each(this.segments,function(segment,index){ | |
segment.transition({ | |
circumference : this.calculateCircumference(segment.value), | |
outerRadius : this.outerRadius, | |
innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout | |
},animDecimal); | |
segment.endAngle = segment.startAngle + segment.circumference; | |
segment.draw(); | |
if (index === 0){ | |
segment.startAngle = Math.PI * 1.5; | |
} | |
//Check to see if it's the last segment, if not get the next and update the start angle | |
if (index < this.segments.length-1){ | |
this.segments[index+1].startAngle = segment.endAngle; | |
} | |
},this); | |
} | |
}); | |
Chart.types.Doughnut.extend({ | |
name : "Pie", | |
defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) | |
}); | |
}).call(this); | |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
///Boolean - Whether grid lines are shown across the chart | |
scaleShowGridLines : true, | |
//String - Colour of the grid lines | |
scaleGridLineColor : "rgba(0,0,0,.05)", | |
//Number - Width of the grid lines | |
scaleGridLineWidth : 1, | |
//Boolean - Whether the line is curved between points | |
bezierCurve : true, | |
//Number - Tension of the bezier curve between points | |
bezierCurveTension : 0.4, | |
//Boolean - Whether to show a dot for each point | |
pointDot : true, | |
//Number - Radius of each point dot in pixels | |
pointDotRadius : 4, | |
//Number - Pixel width of point dot stroke | |
pointDotStrokeWidth : 1, | |
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point | |
pointHitDetectionRadius : 20, | |
//Boolean - Whether to show a stroke for datasets | |
datasetStroke : true, | |
//Number - Pixel width of dataset stroke | |
datasetStrokeWidth : 2, | |
//Boolean - Whether to fill the dataset with a colour | |
datasetFill : true, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
name: "Line", | |
defaults : defaultConfig, | |
initialize: function(data){ | |
//Declare the extension of the default point, to cater for the options passed in to the constructor | |
this.PointClass = Chart.Point.extend({ | |
strokeWidth : this.options.pointDotStrokeWidth, | |
radius : this.options.pointDotRadius, | |
display: this.options.pointDot, | |
hitDetectionRadius : this.options.pointHitDetectionRadius, | |
ctx : this.chart.ctx, | |
inRange : function(mouseX){ | |
return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); | |
} | |
}); | |
this.datasets = []; | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; | |
this.eachPoints(function(point){ | |
point.restore(['fillColor', 'strokeColor']); | |
}); | |
helpers.each(activePoints, function(activePoint){ | |
activePoint.fillColor = activePoint.highlightFill; | |
activePoint.strokeColor = activePoint.highlightStroke; | |
}); | |
this.showTooltip(activePoints); | |
}); | |
} | |
//Iterate through each of the datasets, and build this into a property of the chart | |
helpers.each(data.datasets,function(dataset){ | |
var datasetObject = { | |
label : dataset.label || null, | |
fillColor : dataset.fillColor, | |
strokeColor : dataset.strokeColor, | |
pointColor : dataset.pointColor, | |
pointStrokeColor : dataset.pointStrokeColor, | |
points : [] | |
}; | |
this.datasets.push(datasetObject); | |
helpers.each(dataset.data,function(dataPoint,index){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
datasetObject.points.push(new this.PointClass({ | |
value : dataPoint, | |
label : data.labels[index], | |
datasetLabel: dataset.label, | |
strokeColor : dataset.pointStrokeColor, | |
fillColor : dataset.pointColor, | |
highlightFill : dataset.pointHighlightFill || dataset.pointColor, | |
highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor | |
})); | |
},this); | |
this.buildScale(data.labels); | |
this.eachPoints(function(point, index){ | |
helpers.extend(point, { | |
x: this.scale.calculateX(index), | |
y: this.scale.endPoint | |
}); | |
point.save(); | |
}, this); | |
},this); | |
this.render(); | |
}, | |
update : function(){ | |
this.scale.update(); | |
// Reset any highlight colours before updating. | |
helpers.each(this.activeElements, function(activeElement){ | |
activeElement.restore(['fillColor', 'strokeColor']); | |
}); | |
this.eachPoints(function(point){ | |
point.save(); | |
}); | |
this.render(); | |
}, | |
eachPoints : function(callback){ | |
helpers.each(this.datasets,function(dataset){ | |
helpers.each(dataset.points,callback,this); | |
},this); | |
}, | |
getPointsAtEvent : function(e){ | |
var pointsArray = [], | |
eventPosition = helpers.getRelativePosition(e); | |
helpers.each(this.datasets,function(dataset){ | |
helpers.each(dataset.points,function(point){ | |
if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); | |
}); | |
},this); | |
return pointsArray; | |
}, | |
buildScale : function(labels){ | |
var self = this; | |
var dataTotal = function(){ | |
var values = []; | |
self.eachPoints(function(point){ | |
values.push(point.value); | |
}); | |
return values; | |
}; | |
var scaleOptions = { | |
templateString : this.options.scaleLabel, | |
height : this.chart.height, | |
width : this.chart.width, | |
ctx : this.chart.ctx, | |
textColor : this.options.scaleFontColor, | |
fontSize : this.options.scaleFontSize, | |
fontStyle : this.options.scaleFontStyle, | |
fontFamily : this.options.scaleFontFamily, | |
valuesCount : labels.length, | |
beginAtZero : this.options.scaleBeginAtZero, | |
integersOnly : this.options.scaleIntegersOnly, | |
calculateYRange : function(currentHeight){ | |
var updatedRanges = helpers.calculateScaleRange( | |
dataTotal(), | |
currentHeight, | |
this.fontSize, | |
this.beginAtZero, | |
this.integersOnly | |
); | |
helpers.extend(this, updatedRanges); | |
}, | |
xLabels : labels, | |
font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), | |
lineWidth : this.options.scaleLineWidth, | |
lineColor : this.options.scaleLineColor, | |
gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, | |
gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", | |
padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, | |
showLabels : this.options.scaleShowLabels, | |
display : this.options.showScale | |
}; | |
if (this.options.scaleOverride){ | |
helpers.extend(scaleOptions, { | |
calculateYRange: helpers.noop, | |
steps: this.options.scaleSteps, | |
stepValue: this.options.scaleStepWidth, | |
min: this.options.scaleStartValue, | |
max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) | |
}); | |
} | |
this.scale = new Chart.Scale(scaleOptions); | |
}, | |
addData : function(valuesArray,label){ | |
//Map the values array for each of the datasets | |
helpers.each(valuesArray,function(value,datasetIndex){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
this.datasets[datasetIndex].points.push(new this.PointClass({ | |
value : value, | |
label : label, | |
x: this.scale.calculateX(this.scale.valuesCount+1), | |
y: this.scale.endPoint, | |
strokeColor : this.datasets[datasetIndex].pointStrokeColor, | |
fillColor : this.datasets[datasetIndex].pointColor | |
})); | |
},this); | |
this.scale.addXLabel(label); | |
//Then re-render the chart. | |
this.update(); | |
}, | |
removeData : function(){ | |
this.scale.removeXLabel(); | |
//Then re-render the chart. | |
helpers.each(this.datasets,function(dataset){ | |
dataset.points.shift(); | |
},this); | |
this.update(); | |
}, | |
reflow : function(){ | |
var newScaleProps = helpers.extend({ | |
height : this.chart.height, | |
width : this.chart.width | |
}); | |
this.scale.update(newScaleProps); | |
}, | |
draw : function(ease){ | |
var easingDecimal = ease || 1; | |
this.clear(); | |
var ctx = this.chart.ctx; | |
// Some helper methods for getting the next/prev points | |
var hasValue = function(item){ | |
return item.value !== null; | |
}, | |
nextPoint = function(point, collection, index){ | |
return helpers.findNextWhere(collection, hasValue, index) || point; | |
}, | |
previousPoint = function(point, collection, index){ | |
return helpers.findPreviousWhere(collection, hasValue, index) || point; | |
}; | |
this.scale.draw(easingDecimal); | |
helpers.each(this.datasets,function(dataset){ | |
var pointsWithValues = helpers.where(dataset.points, hasValue); | |
//Transition each point first so that the line and point drawing isn't out of sync | |
//We can use this extra loop to calculate the control points of this dataset also in this loop | |
helpers.each(dataset.points, function(point, index){ | |
if (point.hasValue()){ | |
point.transition({ | |
y : this.scale.calculateY(point.value), | |
x : this.scale.calculateX(index) | |
}, easingDecimal); | |
} | |
},this); | |
// Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point | |
// This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed | |
if (this.options.bezierCurve){ | |
helpers.each(pointsWithValues, function(point, index){ | |
var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; | |
point.controlPoints = helpers.splineCurve( | |
previousPoint(point, pointsWithValues, index), | |
point, | |
nextPoint(point, pointsWithValues, index), | |
tension | |
); | |
// Prevent the bezier going outside of the bounds of the graph | |
// Cap puter bezier handles to the upper/lower scale bounds | |
if (point.controlPoints.outer.y > this.scale.endPoint){ | |
point.controlPoints.outer.y = this.scale.endPoint; | |
} | |
else if (point.controlPoints.outer.y < this.scale.startPoint){ | |
point.controlPoints.outer.y = this.scale.startPoint; | |
} | |
// Cap inner bezier handles to the upper/lower scale bounds | |
if (point.controlPoints.inner.y > this.scale.endPoint){ | |
point.controlPoints.inner.y = this.scale.endPoint; | |
} | |
else if (point.controlPoints.inner.y < this.scale.startPoint){ | |
point.controlPoints.inner.y = this.scale.startPoint; | |
} | |
},this); | |
} | |
//Draw the line between all the points | |
ctx.lineWidth = this.options.datasetStrokeWidth; | |
ctx.strokeStyle = dataset.strokeColor; | |
ctx.beginPath(); | |
helpers.each(pointsWithValues, function(point, index){ | |
if (index === 0){ | |
ctx.moveTo(point.x, point.y); | |
} | |
else{ | |
if(this.options.bezierCurve){ | |
var previous = previousPoint(point, pointsWithValues, index); | |
ctx.bezierCurveTo( | |
previous.controlPoints.outer.x, | |
previous.controlPoints.outer.y, | |
point.controlPoints.inner.x, | |
point.controlPoints.inner.y, | |
point.x, | |
point.y | |
); | |
} | |
else{ | |
ctx.lineTo(point.x,point.y); | |
} | |
} | |
}, this); | |
ctx.stroke(); | |
if (this.options.datasetFill && pointsWithValues.length > 0){ | |
//Round off the line by going to the base of the chart, back to the start, then fill. | |
ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); | |
ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); | |
ctx.fillStyle = dataset.fillColor; | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
//Now draw the points over the line | |
//A little inefficient double looping, but better than the line | |
//lagging behind the point positions | |
helpers.each(pointsWithValues,function(point){ | |
point.draw(); | |
}); | |
},this); | |
} | |
}); | |
}).call(this); | |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
//Cache a local reference to Chart.helpers | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
//Boolean - Show a backdrop to the scale label | |
scaleShowLabelBackdrop : true, | |
//String - The colour of the label backdrop | |
scaleBackdropColor : "rgba(255,255,255,0.75)", | |
// Boolean - Whether the scale should begin at zero | |
scaleBeginAtZero : true, | |
//Number - The backdrop padding above & below the label in pixels | |
scaleBackdropPaddingY : 2, | |
//Number - The backdrop padding to the side of the label in pixels | |
scaleBackdropPaddingX : 2, | |
//Boolean - Show line for each value in the scale | |
scaleShowLine : true, | |
//Boolean - Stroke a line around each segment in the chart | |
segmentShowStroke : true, | |
//String - The colour of the stroke on each segement. | |
segmentStrokeColor : "#fff", | |
//Number - The width of the stroke value in pixels | |
segmentStrokeWidth : 2, | |
//Number - Amount of animation steps | |
animationSteps : 100, | |
//String - Animation easing effect. | |
animationEasing : "easeOutBounce", | |
//Boolean - Whether to animate the rotation of the chart | |
animateRotate : true, | |
//Boolean - Whether to animate scaling the chart from the centre | |
animateScale : false, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
//Passing in a name registers this chart in the Chart namespace | |
name: "PolarArea", | |
//Providing a defaults will also register the deafults in the chart namespace | |
defaults : defaultConfig, | |
//Initialize is fired when the chart is initialized - Data is passed in as a parameter | |
//Config is automatically merged by the core of Chart.js, and is available at this.options | |
initialize: function(data){ | |
this.segments = []; | |
//Declare segment class as a chart instance specific class, so it can share props for this instance | |
this.SegmentArc = Chart.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 Chart.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: true, | |
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: data.length | |
}); | |
this.updateScaleRange(data); | |
this.scale.update(); | |
helpers.each(data,function(segment,index){ | |
this.addData(segment,index,true); | |
},this); | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; | |
helpers.each(this.segments,function(segment){ | |
segment.restore(["fillColor"]); | |
}); | |
helpers.each(activeSegments,function(activeSegment){ | |
activeSegment.fillColor = activeSegment.highlightColor; | |
}); | |
this.showTooltip(activeSegments); | |
}); | |
} | |
this.render(); | |
}, | |
getSegmentsAtEvent : function(e){ | |
var segmentsArray = []; | |
var location = helpers.getRelativePosition(e); | |
helpers.each(this.segments,function(segment){ | |
if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); | |
},this); | |
return segmentsArray; | |
}, | |
addData : function(segment, atIndex, silent){ | |
var index = atIndex || this.segments.length; | |
this.segments.splice(index, 0, new this.SegmentArc({ | |
fillColor: segment.color, | |
highlightColor: segment.highlight || segment.color, | |
label: segment.label, | |
value: segment.value, | |
outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), | |
circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), | |
startAngle: Math.PI * 1.5 | |
})); | |
if (!silent){ | |
this.reflow(); | |
this.update(); | |
} | |
}, | |
removeData: function(atIndex){ | |
var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; | |
this.segments.splice(indexToDelete, 1); | |
this.reflow(); | |
this.update(); | |
}, | |
calculateTotal: function(data){ | |
this.total = 0; | |
helpers.each(data,function(segment){ | |
this.total += segment.value; | |
},this); | |
this.scale.valuesCount = this.segments.length; | |
}, | |
updateScaleRange: function(datapoints){ | |
var valuesArray = []; | |
helpers.each(datapoints,function(segment){ | |
valuesArray.push(segment.value); | |
}); | |
var scaleSizes = (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) | |
} : | |
helpers.calculateScaleRange( | |
valuesArray, | |
helpers.min([this.chart.width, this.chart.height])/2, | |
this.options.scaleFontSize, | |
this.options.scaleBeginAtZero, | |
this.options.scaleIntegersOnly | |
); | |
helpers.extend( | |
this.scale, | |
scaleSizes, | |
{ | |
size: helpers.min([this.chart.width, this.chart.height]), | |
xCenter: this.chart.width/2, | |
yCenter: this.chart.height/2 | |
} | |
); | |
}, | |
update : function(){ | |
this.calculateTotal(this.segments); | |
helpers.each(this.segments,function(segment){ | |
segment.save(); | |
}); | |
this.render(); | |
}, | |
reflow : function(){ | |
helpers.extend(this.SegmentArc.prototype,{ | |
x : this.chart.width/2, | |
y : this.chart.height/2 | |
}); | |
this.updateScaleRange(this.segments); | |
this.scale.update(); | |
helpers.extend(this.scale,{ | |
xCenter: this.chart.width/2, | |
yCenter: this.chart.height/2 | |
}); | |
helpers.each(this.segments, function(segment){ | |
segment.update({ | |
outerRadius : this.scale.calculateCenterOffset(segment.value) | |
}); | |
}, this); | |
}, | |
draw : function(ease){ | |
var easingDecimal = ease || 1; | |
//Clear & draw the canvas | |
this.clear(); | |
helpers.each(this.segments,function(segment, index){ | |
segment.transition({ | |
circumference : this.scale.getCircumference(), | |
outerRadius : this.scale.calculateCenterOffset(segment.value) | |
},easingDecimal); | |
segment.endAngle = segment.startAngle + segment.circumference; | |
// If we've removed the first segment we need to set the first one to | |
// start at the top. | |
if (index === 0){ | |
segment.startAngle = Math.PI * 1.5; | |
} | |
//Check to see if it's the last segment, if not get the next and update the start angle | |
if (index < this.segments.length - 1){ | |
this.segments[index+1].startAngle = segment.endAngle; | |
} | |
segment.draw(); | |
}, this); | |
this.scale.draw(); | |
} | |
}); | |
}).call(this); | |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
helpers = Chart.helpers; | |
Chart.Type.extend({ | |
name: "Radar", | |
defaults:{ | |
//Boolean - Whether to show lines for each scale point | |
scaleShowLine : true, | |
//Boolean - Whether we show the angle lines out of the radar | |
angleShowLineOut : true, | |
//Boolean - Whether to show labels on the scale | |
scaleShowLabels : false, | |
// Boolean - Whether the scale should begin at zero | |
scaleBeginAtZero : true, | |
//String - Colour of the angle line | |
angleLineColor : "rgba(0,0,0,.1)", | |
//Number - Pixel width of the angle line | |
angleLineWidth : 1, | |
//String - Point label font declaration | |
pointLabelFontFamily : "'Arial'", | |
//String - Point label font weight | |
pointLabelFontStyle : "normal", | |
//Number - Point label font size in pixels | |
pointLabelFontSize : 10, | |
//String - Point label font colour | |
pointLabelFontColor : "#666", | |
//Boolean - Whether to show a dot for each point | |
pointDot : true, | |
//Number - Radius of each point dot in pixels | |
pointDotRadius : 3, | |
//Number - Pixel width of point dot stroke | |
pointDotStrokeWidth : 1, | |
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point | |
pointHitDetectionRadius : 20, | |
//Boolean - Whether to show a stroke for datasets | |
datasetStroke : true, | |
//Number - Pixel width of dataset stroke | |
datasetStrokeWidth : 2, | |
//Boolean - Whether to fill the dataset with a colour | |
datasetFill : true, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" | |
}, | |
initialize: function(data){ | |
this.PointClass = Chart.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(data); | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; | |
this.eachPoints(function(point){ | |
point.restore(['fillColor', 'strokeColor']); | |
}); | |
helpers.each(activePointsCollection, function(activePoint){ | |
activePoint.fillColor = activePoint.highlightFill; | |
activePoint.strokeColor = activePoint.highlightStroke; | |
}); | |
this.showTooltip(activePointsCollection); | |
}); | |
} | |
//Iterate through each of the datasets, and build this into a property of the chart | |
helpers.each(data.datasets,function(dataset){ | |
var datasetObject = { | |
label: dataset.label || null, | |
fillColor : dataset.fillColor, | |
strokeColor : dataset.strokeColor, | |
pointColor : dataset.pointColor, | |
pointStrokeColor : dataset.pointStrokeColor, | |
points : [] | |
}; | |
this.datasets.push(datasetObject); | |
helpers.each(dataset.data,function(dataPoint,index){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
var pointPosition; | |
if (!this.scale.animation){ | |
pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); | |
} | |
datasetObject.points.push(new this.PointClass({ | |
value : dataPoint, | |
label : data.labels[index], | |
datasetLabel: dataset.label, | |
x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, | |
y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, | |
strokeColor : dataset.pointStrokeColor, | |
fillColor : dataset.pointColor, | |
highlightFill : dataset.pointHighlightFill || dataset.pointColor, | |
highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor | |
})); | |
},this); | |
},this); | |
this.render(); | |
}, | |
eachPoints : function(callback){ | |
helpers.each(this.datasets,function(dataset){ | |
helpers.each(dataset.points,callback,this); | |
},this); | |
}, | |
getPointsAtEvent : function(evt){ | |
var mousePosition = helpers.getRelativePosition(evt), | |
fromCenter = helpers.getAngleFromPoint({ | |
x: this.scale.xCenter, | |
y: this.scale.yCenter | |
}, mousePosition); | |
var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, | |
pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), | |
activePointsCollection = []; | |
// If we're at the top, make the pointIndex 0 to get the first of the array. | |
if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ | |
pointIndex = 0; | |
} | |
if (fromCenter.distance <= this.scale.drawingArea){ | |
helpers.each(this.datasets, function(dataset){ | |
activePointsCollection.push(dataset.points[pointIndex]); | |
}); | |
} | |
return activePointsCollection; | |
}, | |
buildScale : function(data){ | |
this.scale = new Chart.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, | |
// Point labels at the edge of each line | |
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: data.labels, | |
valuesCount: data.datasets[0].data.length | |
}); | |
this.scale.setScaleSize(); | |
this.updateScaleRange(data.datasets); | |
this.scale.buildYLabels(); | |
}, | |
updateScaleRange: function(datasets){ | |
var valuesArray = (function(){ | |
var totalDataArray = []; | |
helpers.each(datasets,function(dataset){ | |
if (dataset.data){ | |
totalDataArray = totalDataArray.concat(dataset.data); | |
} | |
else { | |
helpers.each(dataset.points, function(point){ | |
totalDataArray.push(point.value); | |
}); | |
} | |
}); | |
return totalDataArray; | |
})(); | |
var scaleSizes = (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) | |
} : | |
helpers.calculateScaleRange( | |
valuesArray, | |
helpers.min([this.chart.width, this.chart.height])/2, | |
this.options.scaleFontSize, | |
this.options.scaleBeginAtZero, | |
this.options.scaleIntegersOnly | |
); | |
helpers.extend( | |
this.scale, | |
scaleSizes | |
); | |
}, | |
addData : function(valuesArray,label){ | |
//Map the values array for each of the datasets | |
this.scale.valuesCount++; | |
helpers.each(valuesArray,function(value,datasetIndex){ | |
var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); | |
this.datasets[datasetIndex].points.push(new this.PointClass({ | |
value : value, | |
label : label, | |
x: pointPosition.x, | |
y: pointPosition.y, | |
strokeColor : this.datasets[datasetIndex].pointStrokeColor, | |
fillColor : this.datasets[datasetIndex].pointColor | |
})); | |
},this); | |
this.scale.labels.push(label); | |
this.reflow(); | |
this.update(); | |
}, | |
removeData : function(){ | |
this.scale.valuesCount--; | |
this.scale.labels.shift(); | |
helpers.each(this.datasets,function(dataset){ | |
dataset.points.shift(); | |
},this); | |
this.reflow(); | |
this.update(); | |
}, | |
update : function(){ | |
this.eachPoints(function(point){ | |
point.save(); | |
}); | |
this.reflow(); | |
this.render(); | |
}, | |
reflow: function(){ | |
helpers.extend(this.scale, { | |
width : this.chart.width, | |
height: this.chart.height, | |
size : helpers.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(ease){ | |
var easeDecimal = ease || 1, | |
ctx = this.chart.ctx; | |
this.clear(); | |
this.scale.draw(); | |
helpers.each(this.datasets,function(dataset){ | |
//Transition each point first so that the line and point drawing isn't out of sync | |
helpers.each(dataset.points,function(point,index){ | |
if (point.hasValue()){ | |
point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); | |
} | |
},this); | |
//Draw the line between all the points | |
ctx.lineWidth = this.options.datasetStrokeWidth; | |
ctx.strokeStyle = dataset.strokeColor; | |
ctx.beginPath(); | |
helpers.each(dataset.points,function(point,index){ | |
if (index === 0){ | |
ctx.moveTo(point.x,point.y); | |
} | |
else{ | |
ctx.lineTo(point.x,point.y); | |
} | |
},this); | |
ctx.closePath(); | |
ctx.stroke(); | |
ctx.fillStyle = dataset.fillColor; | |
ctx.fill(); | |
//Now draw the points over the line | |
//A little inefficient double looping, but better than the line | |
//lagging behind the point positions | |
helpers.each(dataset.points,function(point){ | |
if (point.hasValue()){ | |
point.draw(); | |
} | |
}); | |
},this); | |
} | |
}); | |
}).call(this); |
/*! | |
* Chart.js | |
* http://chartjs.org/ | |
* Version: 1.0.1-beta.4 | |
* | |
* Copyright 2014 Nick Downie | |
* Released under the MIT license | |
* https://github.com/nnnick/Chart.js/blob/master/LICENSE.md | |
*/ | |
(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;this.width=t.canvas.width,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,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<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=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 t.define&&t.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}),x=s.radians=function(t){return t*(Math.PI/180)},S=(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),x=Math.round(f/v);(x>a||a>2*x)&&!h;)if(x>a)v*=2,x=Math.round(f/v),x%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,x=Math.round(f/v)}else v/=2,x=Math.round(f/v);return h&&(x=o,v=f/x),{steps:x,stepValue:v,min:p,max:p+x*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)}),b=(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),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.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-b.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*b.easeInBounce(2*t):.5*b.easeOutBounce(2*t-1)+.5}}),w=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)}}(),s.animationLoop=function(t,i,e,s,n,o){var a=0,h=b[e]||b.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=w(l):n.apply(o)};w(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}),L=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},k=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},P(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){L(t.chart.canvas,e,i)})}),F=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},R=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))}),T=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},M=s.fontString=function(t,i,e){return i+" "+t+"px "+e},W=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},z=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 T(this.chart),this},stop:function(){return s.cancelAnimFrame.call(t,this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=F(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:R(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(),k(this,this.events),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(),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}).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}).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)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=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=M(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=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;switch(t.fillStyle=this.fillColor,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)}z(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=M(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=M(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=W(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(){z(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?W(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(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=W(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(x(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(x(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/(this.valuesCount-(this.offsetGridLines?0: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);t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),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),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)+S(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+S(this.lineWidth),o=this.xLabelRotation>0;t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),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*x(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=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){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<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=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=M(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,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};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<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'}; | |
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*(t/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=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<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};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)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],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){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(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:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};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.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<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},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); |
title | anchor |
---|---|
Getting started |
getting-started |
###Include Chart.js
First we need to include the Chart.js library on the page. The library occupies a global variable of Chart
.
<script src="Chart.js"></script>
Alternatively, if you're using an AMD loader for JavaScript modules, that is also supported in the Chart.js core. Please note: the library will still occupy a global variable of Chart
, even if it detects define
and define.amd
. If this is a problem, you can call noConflict
to restore the global Chart variable to it's previous owner.
// Using requirejs
require(['path/to/Chartjs'], function(Chart){
// Use Chart.js as normal here.
// Chart.noConflict restores the Chart global variable to it's previous owner
// The function returns what was previously Chart, allowing you to reassign.
var Chartjs = Chart.noConflict();
});
You can also grab Chart.js using bower:
bower install chartjs --save
###Creating a chart
To create a chart, we need to instantiate the Chart
class. To do this, we need to pass in the 2d context of where we want to draw the chart. Here's an example.
<canvas id="myChart" width="400" height="400"></canvas>
// Get the context of the canvas element we want to select
var ctx = document.getElementById("myChart").getContext("2d");
var myNewChart = new Chart(ctx).PolarArea(data);
We can also get the context of our canvas with jQuery. To do this, we need to get the DOM node out of the jQuery collection, and call the getContext("2d")
method on that.
// Get context with jQuery - using jQuery's .get() method.
var ctx = $("#myChart").get(0).getContext("2d");
// This will get the first returned node in the jQuery collection.
var myNewChart = new Chart(ctx);
After we've instantiated the Chart class on the canvas we want to draw on, Chart.js will handle the scaling for retina displays.
With the Chart class set up, we can go on to create one of the charts Chart.js has available. In the example below, we would be drawing a Polar area chart.
new Chart(ctx).PolarArea(data, options);
We call a method of the name of the chart we want to create. We pass in the data for that chart type, and the options for that chart as parameters. Chart.js will merge the global defaults with chart type specific defaults, then merge any options passed in as a second argument after data.
###Global chart configuration
This concept was introduced in Chart.js 1.0 to keep configuration DRY, and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type.
Chart.defaults.global = {
// Boolean - Whether to animate the chart
animation: true,
// Number - Number of animation steps
animationSteps: 60,
// String - Animation easing effect
animationEasing: "easeOutQuart",
// Boolean - If we should show the scale at all
showScale: true,
// Boolean - If we want to override with a hard coded scale
scaleOverride: false,
// ** Required if scaleOverride is true **
// Number - The number of steps in a hard coded scale
scaleSteps: null,
// Number - The value jump in the hard coded scale
scaleStepWidth: null,
// Number - The scale starting value
scaleStartValue: null,
// String - Colour of the scale line
scaleLineColor: "rgba(0,0,0,.1)",
// Number - Pixel width of the scale line
scaleLineWidth: 1,
// Boolean - Whether to show labels on the scale
scaleShowLabels: true,
// Interpolated JS string - can access value
scaleLabel: "<%=value%>",
// Boolean - Whether the scale should stick to integers, not floats even if drawing space is there
scaleIntegersOnly: true,
// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
scaleBeginAtZero: false,
// String - Scale label font declaration for the scale label
scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
// Number - Scale label font size in pixels
scaleFontSize: 12,
// String - Scale label font weight style
scaleFontStyle: "normal",
// String - Scale label font colour
scaleFontColor: "#666",
// Boolean - whether or not the chart should be responsive and resize when the browser does.
responsive: false,
// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
maintainAspectRatio: true,
// Boolean - Determines whether to draw tooltips on the canvas or not
showTooltips: true,
// Array - Array of string names to attach tooltip events
tooltipEvents: ["mousemove", "touchstart", "touchmove"],
// String - Tooltip background colour
tooltipFillColor: "rgba(0,0,0,0.8)",
// String - Tooltip label font declaration for the scale label
tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
// Number - Tooltip label font size in pixels
tooltipFontSize: 14,
// String - Tooltip font weight style
tooltipFontStyle: "normal",
// String - Tooltip label font colour
tooltipFontColor: "#fff",
// String - Tooltip title font declaration for the scale label
tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
// Number - Tooltip title font size in pixels
tooltipTitleFontSize: 14,
// String - Tooltip title font weight style
tooltipTitleFontStyle: "bold",
// String - Tooltip title font colour
tooltipTitleFontColor: "#fff",
// Number - pixel width of padding around tooltip text
tooltipYPadding: 6,
// Number - pixel width of padding around tooltip text
tooltipXPadding: 6,
// Number - Size of the caret on the tooltip
tooltipCaretSize: 8,
// Number - Pixel radius of the tooltip border
tooltipCornerRadius: 6,
// Number - Pixel offset from point x to tooltip edge
tooltipXOffset: 10,
{% raw %}
// String - Template string for single tooltips
tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
{% endraw %}
// String - Template string for single tooltips
multiTooltipTemplate: "<%= value %>",
// Function - Will fire on animation progression.
onAnimationProgress: function(){},
// Function - Will fire on animation completion.
onAnimationComplete: function(){}
}
If for example, you wanted all charts created to be responsive, and resize when the browser window does, the following setting can be changed:
Chart.defaults.global.responsive = true;
Now, every time we create a chart, options.responsive
will be true
.
title | anchor |
---|---|
Line Chart |
line-chart |
###Introduction A line chart is a way of plotting data points on a line.
Often, it is used to show trend data, and the comparison of two data sets.
###Example usage
var myLineChart = new Chart(ctx).Line(data, options);
###Data structure
var data = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 81, 56, 55, 40]
},
{
label: "My Second dataset",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [28, 48, 40, 19, 86, 27, 90]
}
]
};
The line chart requires an array of labels for each of the data points. This is shown on the X axis. The data for line charts is broken up into an array of datasets. Each dataset has a colour for the fill, a colour for the line and colours for the points and strokes of the points. These colours are strings just like CSS. You can use RGBA, RGB, HEX or HSL notation.
The label key on each dataset is optional, and can be used when generating a scale for the chart.
These are the customisation options specific to Line charts. These options are merged with the global chart configuration options, and form the options of the chart.
{
///Boolean - Whether grid lines are shown across the chart
scaleShowGridLines : true,
//String - Colour of the grid lines
scaleGridLineColor : "rgba(0,0,0,.05)",
//Number - Width of the grid lines
scaleGridLineWidth : 1,
//Boolean - Whether the line is curved between points
bezierCurve : true,
//Number - Tension of the bezier curve between points
bezierCurveTension : 0.4,
//Boolean - Whether to show a dot for each point
pointDot : true,
//Number - Radius of each point dot in pixels
pointDotRadius : 4,
//Number - Pixel width of point dot stroke
pointDotStrokeWidth : 1,
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
pointHitDetectionRadius : 20,
//Boolean - Whether to show a stroke for datasets
datasetStroke : true,
//Number - Pixel width of dataset stroke
datasetStrokeWidth : 2,
//Boolean - Whether to fill the dataset with a colour
datasetFill : true,
{% raw %}
//String - A legend template
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].lineColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
{% endraw %}
};
You can override these for your Chart
instance by passing a second argument into the Line
method as an object with the keys you want to override.
For example, we could have a line chart without bezier curves between points by doing the following:
new Chart(ctx).Line(data, {
bezierCurve: false
});
// This will create a chart with all of the default options, merged from the global config,
// and the Line chart defaults, but this particular instance will have `bezierCurve` set to false.
We can also change these defaults values for each Line type that is created, this object is available at Chart.defaults.Line
.
Calling getPointsAtEvent(event)
on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event.
canvas.onclick = function(evt){
var activePoints = myLineChart.getPointsAtEvent(evt);
// => activePoints is an array of points on the canvas that are at the same position as the click event.
};
This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application.
Calling update()
on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop.
myLineChart.datasets[0].points[2].value = 50;
// Would update the first dataset's value of 'March' to be 50
myLineChart.update();
// Calling update now animates the position of March from 90 to 50.
Calling addData(valuesArray, label)
on your Chart instance passing an array of values for each dataset, along with a label for those points.
// The values array passed into addData should be one for each dataset in the chart
myLineChart.addData([40, 60], "August");
// This new data will now animate at the end of the chart.
Calling removeData()
on your Chart instance will remove the first value for all datasets on the chart.
myLineChart.removeData();
// The chart will remove the first point and animate other points into place
title | anchor |
---|---|
Bar Chart |
bar-chart |
A bar chart is a way of showing data as bars.
It is sometimes used to show trend data, and the comparison of multiple data sets side by side.
var myBarChart = new Chart(ctx).Bar(data, options);
var data = {
labels: ["January", "February", "March", "April", "May", "June", "July"],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.5)",
strokeColor: "rgba(220,220,220,0.8)",
highlightFill: "rgba(220,220,220,0.75)",
highlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 80, 81, 56, 55, 40]
},
{
label: "My Second dataset",
fillColor: "rgba(151,187,205,0.5)",
strokeColor: "rgba(151,187,205,0.8)",
highlightFill: "rgba(151,187,205,0.75)",
highlightStroke: "rgba(151,187,205,1)",
data: [28, 48, 40, 19, 86, 27, 90]
}
]
};
The bar chart has the a very similar data structure to the line chart, and has an array of datasets, each with colours and an array of data. Again, colours are in CSS format. We have an array of labels too for display. In the example, we are showing the same data as the previous line chart example.
The label key on each dataset is optional, and can be used when generating a scale for the chart.
These are the customisation options specific to Bar charts. These options are merged with the global chart configuration options, and form the options of the chart.
{
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
scaleBeginAtZero : true,
//Boolean - Whether grid lines are shown across the chart
scaleShowGridLines : true,
//String - Colour of the grid lines
scaleGridLineColor : "rgba(0,0,0,.05)",
//Number - Width of the grid lines
scaleGridLineWidth : 1,
//Boolean - If there is a stroke on each bar
barShowStroke : true,
//Number - Pixel width of the bar stroke
barStrokeWidth : 2,
//Number - Spacing between each of the X value sets
barValueSpacing : 5,
//Number - Spacing between data sets within X values
barDatasetSpacing : 1,
{% raw %}
//String - A legend template
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].lineColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
{% endraw %}
}
You can override these for your Chart
instance by passing a second argument into the Bar
method as an object with the keys you want to override.
For example, we could have a bar chart without a stroke on each bar by doing the following:
new Chart(ctx).Bar(data, {
barShowStroke: false
});
// This will create a chart with all of the default options, merged from the global config,
// and the Bar chart defaults but this particular instance will have `barShowStroke` set to false.
We can also change these defaults values for each Bar type that is created, this object is available at Chart.defaults.Bar
.
Calling getBarsAtEvent(event)
on your Chart instance passing an argument of an event, or jQuery event, will return the bar elements that are at that the same position of that event.
canvas.onclick = function(evt){
var activeBars = myBarChart.getBarsAtEvent(evt);
// => activeBars is an array of bars on the canvas that are at the same position as the click event.
};
This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application.
Calling update()
on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop.
myBarChart.datasets[0].bars[2].value = 50;
// Would update the first dataset's value of 'March' to be 50
myBarChart.update();
// Calling update now animates the position of March from 90 to 50.
Calling addData(valuesArray, label)
on your Chart instance passing an array of values for each dataset, along with a label for those bars.
// The values array passed into addData should be one for each dataset in the chart
myBarChart.addData([40, 60], "August");
// The new data will now animate at the end of the chart.
Calling removeData()
on your Chart instance will remove the first value for all datasets on the chart.
myBarChart.removeData();
// The chart will now animate and remove the first bar
title | anchor |
---|---|
Radar Chart |
radar-chart |
###Introduction A radar chart is a way of showing multiple data points and the variation between them.
They are often useful for comparing the points of two or more different data sets.
###Example usage
var myRadarChart = new Chart(ctx).Radar(data, options);
###Data structure
var data = {
labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"],
datasets: [
{
label: "My First dataset",
fillColor: "rgba(220,220,220,0.2)",
strokeColor: "rgba(220,220,220,1)",
pointColor: "rgba(220,220,220,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(220,220,220,1)",
data: [65, 59, 90, 81, 56, 55, 40]
},
{
label: "My Second dataset",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: [28, 48, 40, 19, 96, 27, 100]
}
]
};
For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. For the radar chart data, we have an array of datasets. Each of these is an object, with a fill colour, a stroke colour, a colour for the fill of each point, and a colour for the stroke of each point. We also have an array of data values.
The label key on each dataset is optional, and can be used when generating a scale for the chart.
These are the customisation options specific to Radar charts. These options are merged with the global chart configuration options, and form the options of the chart.
{
//Boolean - Whether to show lines for each scale point
scaleShowLine : true,
//Boolean - Whether we show the angle lines out of the radar
angleShowLineOut : true,
//Boolean - Whether to show labels on the scale
scaleShowLabels : false,
// Boolean - Whether the scale should begin at zero
scaleBeginAtZero : true,
//String - Colour of the angle line
angleLineColor : "rgba(0,0,0,.1)",
//Number - Pixel width of the angle line
angleLineWidth : 1,
//String - Point label font declaration
pointLabelFontFamily : "'Arial'",
//String - Point label font weight
pointLabelFontStyle : "normal",
//Number - Point label font size in pixels
pointLabelFontSize : 10,
//String - Point label font colour
pointLabelFontColor : "#666",
//Boolean - Whether to show a dot for each point
pointDot : true,
//Number - Radius of each point dot in pixels
pointDotRadius : 3,
//Number - Pixel width of point dot stroke
pointDotStrokeWidth : 1,
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point
pointHitDetectionRadius : 20,
//Boolean - Whether to show a stroke for datasets
datasetStroke : true,
//Number - Pixel width of dataset stroke
datasetStrokeWidth : 2,
//Boolean - Whether to fill the dataset with a colour
datasetFill : true,
{% raw %}
//String - A legend template
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].lineColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>"
{% endraw %}
}
You can override these for your Chart
instance by passing a second argument into the Radar
method as an object with the keys you want to override.
For example, we could have a radar chart without a point for each on piece of data by doing the following:
new Chart(ctx).Radar(data, {
pointDot: false
});
// This will create a chart with all of the default options, merged from the global config,
// and the Bar chart defaults but this particular instance will have `pointDot` set to false.
We can also change these defaults values for each Radar type that is created, this object is available at Chart.defaults.Radar
.
Calling getPointsAtEvent(event)
on your Chart instance passing an argument of an event, or jQuery event, will return the point elements that are at that the same position of that event.
canvas.onclick = function(evt){
var activePoints = myRadarChart.getPointsAtEvent(evt);
// => activePoints is an array of points on the canvas that are at the same position as the click event.
};
This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application.
Calling update()
on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop.
myRadarChart.datasets[0].points[2].value = 50;
// Would update the first dataset's value of 'Sleeping' to be 50
myRadarChart.update();
// Calling update now animates the position of Sleeping from 90 to 50.
Calling addData(valuesArray, label)
on your Chart instance passing an array of values for each dataset, along with a label for those points.
// The values array passed into addData should be one for each dataset in the chart
myRadarChart.addData([40, 60], "Dancing");
// The new data will now animate at the end of the chart.
Calling removeData()
on your Chart instance will remove the first value for all datasets on the chart.
myRadarChart.removeData();
// Other points will now animate to their correct positions.
title | anchor |
---|---|
Polar Area Chart |
polar-area-chart |
Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value.
This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context.
new Chart(ctx).PolarArea(data, options);
var data = [
{
value: 300,
color:"#F7464A",
highlight: "#FF5A5E",
label: "Red"
},
{
value: 50,
color: "#46BFBD",
highlight: "#5AD3D1",
label: "Green"
},
{
value: 100,
color: "#FDB45C",
highlight: "#FFC870",
label: "Yellow"
},
{
value: 40,
color: "#949FB1",
highlight: "#A8B3C5",
label: "Grey"
},
{
value: 120,
color: "#4D5360",
highlight: "#616774",
label: "Dark Grey"
}
];
As you can see, for the chart data you pass in an array of objects, with a value and a colour. The value attribute should be a number, while the color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL.
These are the customisation options specific to Polar Area charts. These options are merged with the global chart configuration options, and form the options of the chart.
{
//Boolean - Show a backdrop to the scale label
scaleShowLabelBackdrop : true,
//String - The colour of the label backdrop
scaleBackdropColor : "rgba(255,255,255,0.75)",
// Boolean - Whether the scale should begin at zero
scaleBeginAtZero : true,
//Number - The backdrop padding above & below the label in pixels
scaleBackdropPaddingY : 2,
//Number - The backdrop padding to the side of the label in pixels
scaleBackdropPaddingX : 2,
//Boolean - Show line for each value in the scale
scaleShowLine : true,
//Boolean - Stroke a line around each segment in the chart
segmentShowStroke : true,
//String - The colour of the stroke on each segement.
segmentStrokeColor : "#fff",
//Number - The width of the stroke value in pixels
segmentStrokeWidth : 2,
//Number - Amount of animation steps
animationSteps : 100,
//String - Animation easing effect.
animationEasing : "easeOutBounce",
//Boolean - Whether to animate the rotation of the chart
animateRotate : true,
//Boolean - Whether to animate scaling the chart from the centre
animateScale : false,
{% raw %}
//String - A legend template
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
{% endraw %}
}
You can override these for your Chart
instance by passing a second argument into the PolarArea
method as an object with the keys you want to override.
For example, we could have a polar area chart with a black stroke on each segment like so:
new Chart(ctx).PolarArea(data, {
segmentStrokeColor: "#000000"
});
// This will create a chart with all of the default options, merged from the global config,
// and the PolarArea chart defaults but this particular instance will have `segmentStrokeColor` set to `"#000000"`.
We can also change these defaults values for each PolarArea type that is created, this object is available at Chart.defaults.PolarArea
.
Calling getSegmentsAtEvent(event)
on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at that the same position of that event.
canvas.onclick = function(evt){
var activePoints = myPolarAreaChart.getSegmentsAtEvent(evt);
// => activePoints is an array of segments on the canvas that are at the same position as the click event.
};
This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application.
Calling update()
on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop.
myPolarAreaChart.segments[1].value = 10;
// Would update the first dataset's value of 'Green' to be 10
myPolarAreaChart.update();
// Calling update now animates the position of Green from 50 to 10.
Calling addData(segmentData, index)
on your Chart instance passing an object in the same format as in the constructor. There is an option second argument of 'index', this determines at what index the new segment should be inserted into the chart.
// An object in the same format as the original data source
myPolarAreaChart.addData({
value: 130,
color: "#B48EAD",
highlight: "#C69CBE",
label: "Purple"
});
// The new segment will now animate in.
Calling removeData(index)
on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment.
myPolarAreaChart.removeData();
// Other segments will update to fill the empty space left.
title | anchor |
---|---|
Pie & Doughnut Charts |
doughnut-pie-chart |
###Introduction Pie and doughnut charts are probably the most commonly used chart there are. They are divided into segments, the arc of each segment shows a the proportional value of each piece of data.
They are excellent at showing the relational proportions between data.
Pie and doughnut charts in are effectively the same class in Chart.js, but have one different default value - their percentageInnerCutout
. This equates what percentage of the inner should be cut out. This defaults to 0
for pie charts, and 50
for doughnuts.
They are also registered under two aliases in the Chart
core. Other than their different default value, and different alias, they are exactly the same.
// For a pie chart
var myPieChart = new Chart(ctx[0]).Pie(data,options);
// And for a doughnut chart
var myDoughnutChart = new Chart(ctx[1]).Doughnut(data,options);
var data = [
{
value: 300,
color:"#F7464A",
highlight: "#FF5A5E",
label: "Red"
},
{
value: 50,
color: "#46BFBD",
highlight: "#5AD3D1",
label: "Green"
},
{
value: 100,
color: "#FDB45C",
highlight: "#FFC870",
label: "Yellow"
}
]
For a pie chart, you must pass in an array of objects with a value and a color property. The value attribute should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. The color attribute should be a string. Similar to CSS, for this string you can use HEX notation, RGB, RGBA or HSL.
These are the customisation options specific to Pie & Doughnut charts. These options are merged with the global chart configuration options, and form the options of the chart.
{
//Boolean - Whether we should show a stroke on each segment
segmentShowStroke : true,
//String - The colour of each segment stroke
segmentStrokeColor : "#fff",
//Number - The width of each segment stroke
segmentStrokeWidth : 2,
//Number - The percentage of the chart that we cut out of the middle
percentageInnerCutout : 50, // This is 0 for Pie charts
//Number - Amount of animation steps
animationSteps : 100,
//String - Animation easing effect
animationEasing : "easeOutBounce",
//Boolean - Whether we animate the rotation of the Doughnut
animateRotate : true,
//Boolean - Whether we animate scaling the Doughnut from the centre
animateScale : false,
{% raw %}
//String - A legend template
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>"
{% endraw %}
}
You can override these for your Chart
instance by passing a second argument into the Doughnut
method as an object with the keys you want to override.
For example, we could have a doughnut chart that animates by scaling out from the centre like so:
new Chart(ctx).Doughnut(data, {
animateScale: true
});
// This will create a chart with all of the default options, merged from the global config,
// and the Doughnut chart defaults but this particular instance will have `animateScale` set to `true`.
We can also change these defaults values for each Doughnut type that is created, this object is available at Chart.defaults.Doughnut
. Pie charts also have a clone of these defaults available to change at Chart.defaults.Pie
, with the only difference being percentageInnerCutout
being set to 0.
Calling getSegmentsAtEvent(event)
on your Chart instance passing an argument of an event, or jQuery event, will return the segment elements that are at that the same position of that event.
canvas.onclick = function(evt){
var activePoints = myDoughnutChart.getSegmentsAtEvent(evt);
// => activePoints is an array of segments on the canvas that are at the same position as the click event.
};
This functionality may be useful for implementing DOM based tooltips, or triggering custom behaviour in your application.
Calling update()
on your Chart instance will re-render the chart with any updated values, allowing you to edit the value of multiple existing points, then render those in one animated render loop.
myDoughnutChart.segments[1].value = 10;
// Would update the first dataset's value of 'Green' to be 10
myDoughnutChart.update();
// Calling update now animates the circumference of the segment 'Green' from 50 to 10.
// and transitions other segment widths
Calling addData(segmentData, index)
on your Chart instance passing an object in the same format as in the constructor. There is an option second argument of 'index', this determines at what index the new segment should be inserted into the chart.
// An object in the same format as the original data source
myDoughnutChart.addData({
value: 130,
color: "#B48EAD",
highlight: "#C69CBE",
label: "Purple"
});
// The new segment will now animate in.
Calling removeData(index)
on your Chart instance will remove segment at that particular index. If none is provided, it will default to the last segment.
myDoughnutChart.removeData();
// Other segments will update to fill the empty space left.
title | anchor |
---|---|
Advanced usage |
advanced-usage |
For each chart, there are a set of global prototype methods on the shared ChartType
which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made.
// For example:
var myLineChart = new Chart(ctx).Line(data);
Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful.
// Will clear the canvas that myLineChart is drawn on
myLineChart.clear();
// => returns 'this' for chainability
Use this to stop any current animation loop. This will pause the chart during any current animation frame. Call .render()
to re-animate.
// Stops the charts animation loop at its current frame
myLineChart.stop();
// => returns 'this' for chainability
Use this to manually resize the canvas element. This is run each time the browser is resized, but you can call this method manually if you change the size of the canvas nodes container element.
// Resizes & redraws to fill its container element
myLineChart.resize();
// => returns 'this' for chainability
Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js.
// Destroys a specific chart instance
myLineChart.destroy();
This returns a base 64 encoded string of the chart in it's current state.
myLineChart.toBase64Image();
// => returns png data url of the image on the canvas
Returns an HTML string of a legend for that chart. The template for this legend is at legendTemplate
in the chart options.
myLineChart.generateLegend();
// => returns HTML string of a legend for this chart
Chart.js 1.0 has been rewritten to provide a platform for developers to create their own custom chart types, and be able to share and utilise them through the Chart.js API.
The format is relatively simple, there are a set of utility helper methods under Chart.helpers
, including things such as looping over collections, requesting animation frames, and easing equations.
On top of this, there are also some simple base classes of Chart elements, these all extend from Chart.Element
, and include things such as points, bars and scales.
Chart.Type.extend({
// Passing in a name registers this chart in the Chart namespace
name: "Scatter",
// Providing a defaults will also register the deafults in the chart namespace
defaults : {
options: "Here",
available: "at this.options"
},
// Initialize is fired when the chart is initialized - Data is passed in as a parameter
// Config is automatically merged by the core of Chart.js, and is available at this.options
initialize: function(data){
this.chart.ctx // The drawing context for this chart
this.chart.canvas // the canvas node for this chart
},
// Used to draw something on the canvas
draw: function() {
}
});
// Now we can create a new instance of our chart, using the Chart.js API
new Chart(ctx).Scatter(data);
// initialize is now run
We can also extend existing chart types, and expose them to the API in the same way. Let's say for example, we might want to run some more code when we initialize every Line chart.
// Notice now we're extending the particular Line chart type, rather than the base class.
Chart.types.Line.extend({
// Passing in a name registers this chart in the Chart namespace in the same way
name: "LineAlt",
initialize: function(data){
console.log('My Line chart extension');
Chart.types.Line.prototype.initialize.apply(this, arguments);
}
});
// Creates a line chart in the same way
new Chart(ctx).LineAlt(data);
// but this logs 'My Line chart extension' in the console.
- <a href="https://github.com/Regaddi/Chart.StackedBar.js" target"_blank">Stacked Bar Chart by @Regaddi
Chart.js uses gulp to build the library into a single JavaScript file. We can use this same build script with custom parameters in order to build a custom version.
Firstly, we need to ensure development dependencies are installed. With node and npm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following:
npm install
npm install -g gulp
This will install the local development dependencies for Chart.js, along with a CLI for the JavaScript task runner gulp.
Now, we can run the gulp build
task, and pass in a comma seperated list of types as an argument to build a custom version of Chart.js with only specified chart types.
Here we will create a version of Chart.js with only Line, Radar and Bar charts included:
gulp build --types=Line,Radar,Bar
This will output to the /custom
directory, and write two files, Chart.js, and Chart.min.js with only those chart types included.
title | anchor |
---|---|
Notes |
notes |
Browser support for the canvas element is available in all modern & major mobile browsers (caniuse.com/canvas).
For IE8 & below, I would recommend using the polyfill ExplorerCanvas - available at https://code.google.com/p/explorercanvas/. It falls back to Internet explorer's format VML when canvas support is not available. Example use:
<head>
<!--[if lte IE 8]>
<script src="excanvas.js"></script>
<![endif]-->
</head>
Usually I would recommend feature detection to choose whether or not to load a polyfill, rather than IE conditional comments, however in this case, VML is a Microsoft proprietary format, so it will only work in IE.
Some important points to note in my experience using ExplorerCanvas as a fallback.
- Initialise charts on load rather than DOMContentReady when using the library, as sometimes a race condition will occur, and it will result in an error when trying to get the 2d context of a canvas.
- New VML DOM elements are being created for each animation frame and there is no hardware acceleration. As a result animation is usually slow and jerky, with flashing text. It is a good idea to dynamically turn off animation based on canvas support. I recommend using the excellent Modernizr to do this.
- When declaring fonts, the library explorercanvas requires the font name to be in single quotes inside the string. For example, instead of your scaleFontFamily property being simply "Arial", explorercanvas support, use "'Arial'" instead. Chart.js does this for default values.
Please report these on the GitHub page - at github.com/nnnick/Chart.js. If you could include a link to a simple jsbin or similar to demonstrate the issue, that'd be really helpful.
New contributions to the library are welcome, just a couple of guidelines:
- Tabs for indentation, not spaces please.
- Please ensure you're changing the individual files in
/src
, not the concatenated output in theChart.js
file in the root of the repo. - Please check that your code will pass
jshint
code standards,gulp jshint
will run this for you. - Please keep pull requests concise, and document new functionality in the relevant
.md
file. - Consider whether your changes are useful for all users, or if creating a Chart.js extension would be more appropriate.
Chart.js is open source and available under the MIT license.
var gulp = require('gulp'), | |
concat = require('gulp-concat'), | |
uglify = require('gulp-uglify'), | |
util = require('gulp-util'), | |
jshint = require('gulp-jshint'), | |
size = require('gulp-size'), | |
connect = require('gulp-connect'), | |
replace = require('gulp-replace'), | |
inquirer = require('inquirer'), | |
semver = require('semver'), | |
exec = require('child_process').exec, | |
fs = require('fs'), | |
package = require('./package.json'), | |
bower = require('./bower.json'); | |
var srcDir = './src/'; | |
/* | |
* Usage : gulp build --types=Bar,Line,Doughnut | |
* Output: - A built Chart.js file with Core and types Bar, Line and Doughnut concatenated together | |
* - A minified version of this code, in Chart.min.js | |
*/ | |
gulp.task('build', function(){ | |
// Default to all of the chart types, with Chart.Core first | |
var srcFiles = [FileName('Core')], | |
isCustom = !!(util.env.types), | |
outputDir = (isCustom) ? 'custom' : '.'; | |
if (isCustom){ | |
util.env.types.split(',').forEach(function(type){ return srcFiles.push(FileName(type))}); | |
} | |
else{ | |
// Seems gulp-concat remove duplicates - nice! | |
// So we can use this to sort out dependency order - aka include Core first! | |
srcFiles.push(srcDir+'*'); | |
} | |
return gulp.src(srcFiles) | |
.pipe(concat('Chart.js')) | |
.pipe(replace('{{ version }}', package.version)) | |
.pipe(gulp.dest(outputDir)) | |
.pipe(uglify({preserveComments:'some'})) | |
.pipe(concat('Chart.min.js')) | |
.pipe(gulp.dest(outputDir)); | |
function FileName(moduleName){ | |
return srcDir+'Chart.'+moduleName+'.js'; | |
}; | |
}); | |
/* | |
* Usage : gulp bump | |
* Prompts: Version increment to bump | |
* Output: - New version number written into package.json & bower.json | |
*/ | |
gulp.task('bump', function(complete){ | |
util.log('Current version:', util.colors.cyan(package.version)); | |
var choices = ['major', 'premajor', 'minor', 'preminor', 'patch', 'prepatch', 'prerelease'].map(function(versionType){ | |
return versionType + ' (v' + semver.inc(package.version, versionType) + ')'; | |
}); | |
inquirer.prompt({ | |
type: 'list', | |
name: 'version', | |
message: 'What version update would you like?', | |
choices: choices | |
}, function(res){ | |
var increment = res.version.split(' ')[0], | |
newVersion = semver.inc(package.version, increment); | |
// Set the new versions into the bower/package object | |
package.version = newVersion; | |
bower.version = newVersion; | |
// Write these to their own files, then build the output | |
fs.writeFileSync('package.json', JSON.stringify(package, null, 2)); | |
fs.writeFileSync('bower.json', JSON.stringify(bower, null, 2)); | |
complete(); | |
}); | |
}); | |
gulp.task('release', ['build'], function(){ | |
exec('git tag -a v' + package.version); | |
}); | |
gulp.task('jshint', function(){ | |
return gulp.src(srcDir + '*.js') | |
.pipe(jshint()) | |
.pipe(jshint.reporter('default')); | |
}); | |
gulp.task('library-size', function(){ | |
return gulp.src('Chart.min.js') | |
.pipe(size({ | |
gzip: true | |
})); | |
}); | |
gulp.task('module-sizes', function(){ | |
return gulp.src(srcDir + '*.js') | |
.pipe(uglify({preserveComments:'some'})) | |
.pipe(size({ | |
showFiles: true, | |
gzip: true | |
})) | |
}); | |
gulp.task('watch', function(){ | |
gulp.watch('./src/*', ['build']); | |
}); | |
gulp.task('test', ['jshint']); | |
gulp.task('size', ['library-size', 'module-sizes']); | |
gulp.task('default', ['build', 'watch']); | |
gulp.task('server', function(){ | |
connect.server({ | |
port: 8000, | |
}); | |
}); | |
// Convenience task for opening the project straight from the command line | |
gulp.task('_open', function(){ | |
exec('open http://localhost:8000'); | |
exec('subl .'); | |
}); | |
gulp.task('dev', ['server', 'default']); |
Copyright (c) 2013-2014 Nick Downie
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
{ | |
"name": "chart.js", | |
"homepage": "http://www.chartjs.org", | |
"description": "Simple HTML5 charts using the canvas element.", | |
"version": "1.0.1-beta.4", | |
"main": "Chart.js", | |
"repository": { | |
"type": "git", | |
"url": "https://github.com/nnnick/Chart.js.git" | |
}, | |
"dependences": {}, | |
"devDependencies": { | |
"gulp": "3.5.x", | |
"gulp-concat": "~2.1.x", | |
"gulp-connect": "~2.0.5", | |
"gulp-jshint": "~1.5.1", | |
"gulp-replace": "^0.4.0", | |
"gulp-size": "~0.4.0", | |
"gulp-uglify": "~0.2.x", | |
"gulp-util": "~2.2.x", | |
"inquirer": "^0.5.1", | |
"semver": "^3.0.1" | |
} | |
} |
Simple HTML5 Charts using the canvas element chartjs.org
You can find documentation at chartjs.org/docs. The markdown files that build the site are available under /docs
. Please note - in some of the json examples of configuration you might notice some liquid tags - this is just for the generating the site html, please disregard.
Chart.js is available under the MIT license.
When reporting bugs or issues, if you could include a link to a simple jsbin or similar demonstrating the issue, that'd be really helpful.
New contributions to the library are welcome, just a couple of guidelines:
- Tabs for indentation, not spaces please.
- Please ensure you're changing the individual files in
/src
, not the concatenated output in theChart.js
file in the root of the repo. - Please check that your code will pass
jshint
code standards,gulp jshint
will run this for you. - Please keep pull requests concise, and document new functionality in the relevant
.md
file. - Consider whether your changes are useful for all users, or if creating a Chart.js extension would be more appropriate.
- Please avoid committing in the build Chart.js & Chart.min.js file, as it causes conflicts when merging.
<!doctype html> | |
<html> | |
<head> | |
<title>Bar Chart</title> | |
<script src="../Chart.js"></script> | |
</head> | |
<body> | |
<div style="width: 50%"> | |
<canvas id="canvas" height="450" width="600"></canvas> | |
</div> | |
<script> | |
var randomScalingFactor = function(){ return Math.round(Math.random()*100)}; | |
var barChartData = { | |
labels : ["January","February","March","April","May","June","July"], | |
datasets : [ | |
{ | |
fillColor : "rgba(220,220,220,0.5)", | |
strokeColor : "rgba(220,220,220,0.8)", | |
highlightFill: "rgba(220,220,220,0.75)", | |
highlightStroke: "rgba(220,220,220,1)", | |
data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] | |
}, | |
{ | |
fillColor : "rgba(151,187,205,0.5)", | |
strokeColor : "rgba(151,187,205,0.8)", | |
highlightFill : "rgba(151,187,205,0.75)", | |
highlightStroke : "rgba(151,187,205,1)", | |
data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] | |
} | |
] | |
} | |
window.onload = function(){ | |
var ctx = document.getElementById("canvas").getContext("2d"); | |
window.myBar = new Chart(ctx).Bar(barChartData, { | |
responsive : true | |
}); | |
} | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<html> | |
<head> | |
<title>Doughnut Chart</title> | |
<script src="../Chart.js"></script> | |
<style> | |
body{ | |
padding: 0; | |
margin: 0; | |
} | |
#canvas-holder{ | |
width:30%; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="canvas-holder"> | |
<canvas id="chart-area" width="500" height="500"/> | |
</div> | |
<script> | |
var doughnutData = [ | |
{ | |
value: 300, | |
color:"#F7464A", | |
highlight: "#FF5A5E", | |
label: "Red" | |
}, | |
{ | |
value: 50, | |
color: "#46BFBD", | |
highlight: "#5AD3D1", | |
label: "Green" | |
}, | |
{ | |
value: 100, | |
color: "#FDB45C", | |
highlight: "#FFC870", | |
label: "Yellow" | |
}, | |
{ | |
value: 40, | |
color: "#949FB1", | |
highlight: "#A8B3C5", | |
label: "Grey" | |
}, | |
{ | |
value: 120, | |
color: "#4D5360", | |
highlight: "#616774", | |
label: "Dark Grey" | |
} | |
]; | |
window.onload = function(){ | |
var ctx = document.getElementById("chart-area").getContext("2d"); | |
window.myDoughnut = new Chart(ctx).Doughnut(doughnutData, {responsive : true}); | |
}; | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<html> | |
<head> | |
<title>Line Chart</title> | |
<script src="../Chart.js"></script> | |
</head> | |
<body> | |
<div style="width:30%"> | |
<div> | |
<canvas id="canvas" height="450" width="600"></canvas> | |
</div> | |
</div> | |
<script> | |
var randomScalingFactor = function(){ return Math.round(Math.random()*100)}; | |
var lineChartData = { | |
labels : ["January","February","March","April","May","June","July"], | |
datasets : [ | |
{ | |
label: "My First dataset", | |
fillColor : "rgba(220,220,220,0.2)", | |
strokeColor : "rgba(220,220,220,1)", | |
pointColor : "rgba(220,220,220,1)", | |
pointStrokeColor : "#fff", | |
pointHighlightFill : "#fff", | |
pointHighlightStroke : "rgba(220,220,220,1)", | |
data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] | |
}, | |
{ | |
label: "My Second dataset", | |
fillColor : "rgba(151,187,205,0.2)", | |
strokeColor : "rgba(151,187,205,1)", | |
pointColor : "rgba(151,187,205,1)", | |
pointStrokeColor : "#fff", | |
pointHighlightFill : "#fff", | |
pointHighlightStroke : "rgba(151,187,205,1)", | |
data : [randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor(),randomScalingFactor()] | |
} | |
] | |
} | |
window.onload = function(){ | |
var ctx = document.getElementById("canvas").getContext("2d"); | |
window.myLine = new Chart(ctx).Line(lineChartData, { | |
responsive: true | |
}); | |
} | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<html> | |
<head> | |
<title>Pie Chart</title> | |
<script src="../Chart.js"></script> | |
</head> | |
<body> | |
<div id="canvas-holder"> | |
<canvas id="chart-area" width="300" height="300"/> | |
</div> | |
<script> | |
var pieData = [ | |
{ | |
value: 300, | |
color:"#F7464A", | |
highlight: "#FF5A5E", | |
label: "Red" | |
}, | |
{ | |
value: 50, | |
color: "#46BFBD", | |
highlight: "#5AD3D1", | |
label: "Green" | |
}, | |
{ | |
value: 100, | |
color: "#FDB45C", | |
highlight: "#FFC870", | |
label: "Yellow" | |
}, | |
{ | |
value: 40, | |
color: "#949FB1", | |
highlight: "#A8B3C5", | |
label: "Grey" | |
}, | |
{ | |
value: 120, | |
color: "#4D5360", | |
highlight: "#616774", | |
label: "Dark Grey" | |
} | |
]; | |
window.onload = function(){ | |
var ctx = document.getElementById("chart-area").getContext("2d"); | |
window.myPie = new Chart(ctx).Pie(pieData); | |
}; | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<html> | |
<head> | |
<title>Polar Area Chart</title> | |
<script src="../Chart.js"></script> | |
</head> | |
<body> | |
<div id="canvas-holder" style="width:30%"> | |
<canvas id="chart-area" width="300" height="300"/> | |
</div> | |
<script> | |
var polarData = [ | |
{ | |
value: 300, | |
color:"#F7464A", | |
highlight: "#FF5A5E", | |
label: "Red" | |
}, | |
{ | |
value: 50, | |
color: "#46BFBD", | |
highlight: "#5AD3D1", | |
label: "Green" | |
}, | |
{ | |
value: 100, | |
color: "#FDB45C", | |
highlight: "#FFC870", | |
label: "Yellow" | |
}, | |
{ | |
value: 40, | |
color: "#949FB1", | |
highlight: "#A8B3C5", | |
label: "Grey" | |
}, | |
{ | |
value: 120, | |
color: "#4D5360", | |
highlight: "#616774", | |
label: "Dark Grey" | |
} | |
]; | |
window.onload = function(){ | |
var ctx = document.getElementById("chart-area").getContext("2d"); | |
window.myPolarArea = new Chart(ctx).PolarArea(polarData, { | |
responsive:true | |
}); | |
}; | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<html> | |
<head> | |
<title>Radar Chart</title> | |
<script src="../Chart.js"></script> | |
<meta name = "viewport" content = "initial-scale = 1, user-scalable = no"> | |
<style> | |
canvas{ | |
} | |
</style> | |
</head> | |
<body> | |
<div style="width:30%"> | |
<canvas id="canvas" height="450" width="450"></canvas> | |
</div> | |
<script> | |
var radarChartData = { | |
labels: ["Eating", "Drinking", "Sleeping", "Designing", "Coding", "Cycling", "Running"], | |
datasets: [ | |
{ | |
label: "My First dataset", | |
fillColor: "rgba(220,220,220,0.2)", | |
strokeColor: "rgba(220,220,220,1)", | |
pointColor: "rgba(220,220,220,1)", | |
pointStrokeColor: "#fff", | |
pointHighlightFill: "#fff", | |
pointHighlightStroke: "rgba(220,220,220,1)", | |
data: [65,59,90,81,56,55,40] | |
}, | |
{ | |
label: "My Second dataset", | |
fillColor: "rgba(151,187,205,0.2)", | |
strokeColor: "rgba(151,187,205,1)", | |
pointColor: "rgba(151,187,205,1)", | |
pointStrokeColor: "#fff", | |
pointHighlightFill: "#fff", | |
pointHighlightStroke: "rgba(151,187,205,1)", | |
data: [28,48,40,19,96,27,100] | |
} | |
] | |
}; | |
window.onload = function(){ | |
window.myRadar = new Chart(document.getElementById("canvas").getContext("2d")).Radar(radarChartData, { | |
responsive: true | |
}); | |
} | |
</script> | |
</body> | |
</html> |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value | |
scaleBeginAtZero : true, | |
//Boolean - Whether grid lines are shown across the chart | |
scaleShowGridLines : true, | |
//String - Colour of the grid lines | |
scaleGridLineColor : "rgba(0,0,0,.05)", | |
//Number - Width of the grid lines | |
scaleGridLineWidth : 1, | |
//Boolean - If there is a stroke on each bar | |
barShowStroke : true, | |
//Number - Pixel width of the bar stroke | |
barStrokeWidth : 2, | |
//Number - Spacing between each of the X value sets | |
barValueSpacing : 5, | |
//Number - Spacing between data sets within X values | |
barDatasetSpacing : 1, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].fillColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
name: "Bar", | |
defaults : defaultConfig, | |
initialize: function(data){ | |
//Expose options as a scope variable here so we can access it in the ScaleClass | |
var options = this.options; | |
this.ScaleClass = Chart.Scale.extend({ | |
offsetGridLines : true, | |
calculateBarX : function(datasetCount, datasetIndex, barIndex){ | |
//Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar | |
var xWidth = this.calculateBaseWidth(), | |
xAbsolute = this.calculateX(barIndex) - (xWidth/2), | |
barWidth = this.calculateBarWidth(datasetCount); | |
return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; | |
}, | |
calculateBaseWidth : function(){ | |
return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); | |
}, | |
calculateBarWidth : function(datasetCount){ | |
//The padding between datasets is to the right of each bar, providing that there are more than 1 dataset | |
var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); | |
return (baseWidth / datasetCount); | |
} | |
}); | |
this.datasets = []; | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; | |
this.eachBars(function(bar){ | |
bar.restore(['fillColor', 'strokeColor']); | |
}); | |
helpers.each(activeBars, function(activeBar){ | |
activeBar.fillColor = activeBar.highlightFill; | |
activeBar.strokeColor = activeBar.highlightStroke; | |
}); | |
this.showTooltip(activeBars); | |
}); | |
} | |
//Declare the extension of the default point, to cater for the options passed in to the constructor | |
this.BarClass = Chart.Rectangle.extend({ | |
strokeWidth : this.options.barStrokeWidth, | |
showStroke : this.options.barShowStroke, | |
ctx : this.chart.ctx | |
}); | |
//Iterate through each of the datasets, and build this into a property of the chart | |
helpers.each(data.datasets,function(dataset,datasetIndex){ | |
var datasetObject = { | |
label : dataset.label || null, | |
fillColor : dataset.fillColor, | |
strokeColor : dataset.strokeColor, | |
bars : [] | |
}; | |
this.datasets.push(datasetObject); | |
helpers.each(dataset.data,function(dataPoint,index){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
datasetObject.bars.push(new this.BarClass({ | |
value : dataPoint, | |
label : data.labels[index], | |
datasetLabel: dataset.label, | |
strokeColor : dataset.strokeColor, | |
fillColor : dataset.fillColor, | |
highlightFill : dataset.highlightFill || dataset.fillColor, | |
highlightStroke : dataset.highlightStroke || dataset.strokeColor | |
})); | |
},this); | |
},this); | |
this.buildScale(data.labels); | |
this.BarClass.prototype.base = this.scale.endPoint; | |
this.eachBars(function(bar, index, datasetIndex){ | |
helpers.extend(bar, { | |
width : this.scale.calculateBarWidth(this.datasets.length), | |
x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), | |
y: this.scale.endPoint | |
}); | |
bar.save(); | |
}, this); | |
this.render(); | |
}, | |
update : function(){ | |
this.scale.update(); | |
// Reset any highlight colours before updating. | |
helpers.each(this.activeElements, function(activeElement){ | |
activeElement.restore(['fillColor', 'strokeColor']); | |
}); | |
this.eachBars(function(bar){ | |
bar.save(); | |
}); | |
this.render(); | |
}, | |
eachBars : function(callback){ | |
helpers.each(this.datasets,function(dataset, datasetIndex){ | |
helpers.each(dataset.bars, callback, this, datasetIndex); | |
},this); | |
}, | |
getBarsAtEvent : function(e){ | |
var barsArray = [], | |
eventPosition = helpers.getRelativePosition(e), | |
datasetIterator = function(dataset){ | |
barsArray.push(dataset.bars[barIndex]); | |
}, | |
barIndex; | |
for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { | |
for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { | |
if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ | |
helpers.each(this.datasets, datasetIterator); | |
return barsArray; | |
} | |
} | |
} | |
return barsArray; | |
}, | |
buildScale : function(labels){ | |
var self = this; | |
var dataTotal = function(){ | |
var values = []; | |
self.eachBars(function(bar){ | |
values.push(bar.value); | |
}); | |
return values; | |
}; | |
var scaleOptions = { | |
templateString : this.options.scaleLabel, | |
height : this.chart.height, | |
width : this.chart.width, | |
ctx : this.chart.ctx, | |
textColor : this.options.scaleFontColor, | |
fontSize : this.options.scaleFontSize, | |
fontStyle : this.options.scaleFontStyle, | |
fontFamily : this.options.scaleFontFamily, | |
valuesCount : labels.length, | |
beginAtZero : this.options.scaleBeginAtZero, | |
integersOnly : this.options.scaleIntegersOnly, | |
calculateYRange: function(currentHeight){ | |
var updatedRanges = helpers.calculateScaleRange( | |
dataTotal(), | |
currentHeight, | |
this.fontSize, | |
this.beginAtZero, | |
this.integersOnly | |
); | |
helpers.extend(this, updatedRanges); | |
}, | |
xLabels : labels, | |
font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), | |
lineWidth : this.options.scaleLineWidth, | |
lineColor : this.options.scaleLineColor, | |
gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, | |
gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", | |
padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, | |
showLabels : this.options.scaleShowLabels, | |
display : this.options.showScale | |
}; | |
if (this.options.scaleOverride){ | |
helpers.extend(scaleOptions, { | |
calculateYRange: helpers.noop, | |
steps: this.options.scaleSteps, | |
stepValue: this.options.scaleStepWidth, | |
min: this.options.scaleStartValue, | |
max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) | |
}); | |
} | |
this.scale = new this.ScaleClass(scaleOptions); | |
}, | |
addData : function(valuesArray,label){ | |
//Map the values array for each of the datasets | |
helpers.each(valuesArray,function(value,datasetIndex){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
this.datasets[datasetIndex].bars.push(new this.BarClass({ | |
value : value, | |
label : label, | |
x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), | |
y: this.scale.endPoint, | |
width : this.scale.calculateBarWidth(this.datasets.length), | |
base : this.scale.endPoint, | |
strokeColor : this.datasets[datasetIndex].strokeColor, | |
fillColor : this.datasets[datasetIndex].fillColor | |
})); | |
},this); | |
this.scale.addXLabel(label); | |
//Then re-render the chart. | |
this.update(); | |
}, | |
removeData : function(){ | |
this.scale.removeXLabel(); | |
//Then re-render the chart. | |
helpers.each(this.datasets,function(dataset){ | |
dataset.bars.shift(); | |
},this); | |
this.update(); | |
}, | |
reflow : function(){ | |
helpers.extend(this.BarClass.prototype,{ | |
y: this.scale.endPoint, | |
base : this.scale.endPoint | |
}); | |
var newScaleProps = helpers.extend({ | |
height : this.chart.height, | |
width : this.chart.width | |
}); | |
this.scale.update(newScaleProps); | |
}, | |
draw : function(ease){ | |
var easingDecimal = ease || 1; | |
this.clear(); | |
var ctx = this.chart.ctx; | |
this.scale.draw(easingDecimal); | |
//Draw all the bars for each dataset | |
helpers.each(this.datasets,function(dataset,datasetIndex){ | |
helpers.each(dataset.bars,function(bar,index){ | |
if (bar.hasValue()){ | |
bar.base = this.scale.endPoint; | |
//Transition then draw | |
bar.transition({ | |
x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), | |
y : this.scale.calculateY(bar.value), | |
width : this.scale.calculateBarWidth(this.datasets.length) | |
}, easingDecimal).draw(); | |
} | |
},this); | |
},this); | |
} | |
}); | |
}).call(this); |
/*! | |
* Chart.js | |
* http://chartjs.org/ | |
* Version: {{ version }} | |
* | |
* Copyright 2014 Nick Downie | |
* Released under the MIT license | |
* https://github.com/nnnick/Chart.js/blob/master/LICENSE.md | |
*/ | |
(function(){ | |
"use strict"; | |
//Declare root variable - window in the browser, global on the server | |
var root = this, | |
previous = root.Chart; | |
//Occupy the global variable of Chart, and create a simple base class | |
var Chart = function(context){ | |
var chart = this; | |
this.canvas = context.canvas; | |
this.ctx = context; | |
//Variables global to the chart | |
var width = this.width = context.canvas.width; | |
var height = this.height = context.canvas.height; | |
this.aspectRatio = this.width / this.height; | |
//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale. | |
helpers.retinaScale(this); | |
return this; | |
}; | |
//Globally expose the defaults to allow for user updating/changing | |
Chart.defaults = { | |
global: { | |
// Boolean - Whether to animate the chart | |
animation: true, | |
// Number - Number of animation steps | |
animationSteps: 60, | |
// String - Animation easing effect | |
animationEasing: "easeOutQuart", | |
// Boolean - If we should show the scale at all | |
showScale: true, | |
// Boolean - If we want to override with a hard coded scale | |
scaleOverride: false, | |
// ** Required if scaleOverride is true ** | |
// Number - The number of steps in a hard coded scale | |
scaleSteps: null, | |
// Number - The value jump in the hard coded scale | |
scaleStepWidth: null, | |
// Number - The scale starting value | |
scaleStartValue: null, | |
// String - Colour of the scale line | |
scaleLineColor: "rgba(0,0,0,.1)", | |
// Number - Pixel width of the scale line | |
scaleLineWidth: 1, | |
// Boolean - Whether to show labels on the scale | |
scaleShowLabels: true, | |
// Interpolated JS string - can access value | |
scaleLabel: "<%=value%>", | |
// Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there | |
scaleIntegersOnly: true, | |
// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value | |
scaleBeginAtZero: false, | |
// String - Scale label font declaration for the scale label | |
scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", | |
// Number - Scale label font size in pixels | |
scaleFontSize: 12, | |
// String - Scale label font weight style | |
scaleFontStyle: "normal", | |
// String - Scale label font colour | |
scaleFontColor: "#666", | |
// Boolean - whether or not the chart should be responsive and resize when the browser does. | |
responsive: false, | |
// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container | |
maintainAspectRatio: true, | |
// Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove | |
showTooltips: true, | |
// Array - Array of string names to attach tooltip events | |
tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"], | |
// String - Tooltip background colour | |
tooltipFillColor: "rgba(0,0,0,0.8)", | |
// String - Tooltip label font declaration for the scale label | |
tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", | |
// Number - Tooltip label font size in pixels | |
tooltipFontSize: 14, | |
// String - Tooltip font weight style | |
tooltipFontStyle: "normal", | |
// String - Tooltip label font colour | |
tooltipFontColor: "#fff", | |
// String - Tooltip title font declaration for the scale label | |
tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", | |
// Number - Tooltip title font size in pixels | |
tooltipTitleFontSize: 14, | |
// String - Tooltip title font weight style | |
tooltipTitleFontStyle: "bold", | |
// String - Tooltip title font colour | |
tooltipTitleFontColor: "#fff", | |
// Number - pixel width of padding around tooltip text | |
tooltipYPadding: 6, | |
// Number - pixel width of padding around tooltip text | |
tooltipXPadding: 6, | |
// Number - Size of the caret on the tooltip | |
tooltipCaretSize: 8, | |
// Number - Pixel radius of the tooltip border | |
tooltipCornerRadius: 6, | |
// Number - Pixel offset from point x to tooltip edge | |
tooltipXOffset: 10, | |
// String - Template string for single tooltips | |
tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>", | |
// String - Template string for single tooltips | |
multiTooltipTemplate: "<%= value %>", | |
// String - Colour behind the legend colour block | |
multiTooltipKeyBackground: '#fff', | |
// Function - Will fire on animation progression. | |
onAnimationProgress: function(){}, | |
// Function - Will fire on animation completion. | |
onAnimationComplete: function(){} | |
} | |
}; | |
//Create a dictionary of chart types, to allow for extension of existing types | |
Chart.types = {}; | |
//Global Chart helpers object for utility methods and classes | |
var helpers = Chart.helpers = {}; | |
//-- Basic js utility methods | |
var each = helpers.each = function(loopable,callback,self){ | |
var additionalArgs = Array.prototype.slice.call(arguments, 3); | |
// Check to see if null or undefined firstly. | |
if (loopable){ | |
if (loopable.length === +loopable.length){ | |
var i; | |
for (i=0; i<loopable.length; i++){ | |
callback.apply(self,[loopable[i], i].concat(additionalArgs)); | |
} | |
} | |
else{ | |
for (var item in loopable){ | |
callback.apply(self,[loopable[item],item].concat(additionalArgs)); | |
} | |
} | |
} | |
}, | |
clone = helpers.clone = function(obj){ | |
var objClone = {}; | |
each(obj,function(value,key){ | |
if (obj.hasOwnProperty(key)) objClone[key] = value; | |
}); | |
return objClone; | |
}, | |
extend = helpers.extend = function(base){ | |
each(Array.prototype.slice.call(arguments,1), function(extensionObject) { | |
each(extensionObject,function(value,key){ | |
if (extensionObject.hasOwnProperty(key)) base[key] = value; | |
}); | |
}); | |
return base; | |
}, | |
merge = helpers.merge = function(base,master){ | |
//Merge properties in left object over to a shallow clone of object right. | |
var args = Array.prototype.slice.call(arguments,0); | |
args.unshift({}); | |
return extend.apply(null, args); | |
}, | |
indexOf = helpers.indexOf = function(arrayToSearch, item){ | |
if (Array.prototype.indexOf) { | |
return arrayToSearch.indexOf(item); | |
} | |
else{ | |
for (var i = 0; i < arrayToSearch.length; i++) { | |
if (arrayToSearch[i] === item) return i; | |
} | |
return -1; | |
} | |
}, | |
where = helpers.where = function(collection, filterCallback){ | |
var filtered = []; | |
helpers.each(collection, function(item){ | |
if (filterCallback(item)){ | |
filtered.push(item); | |
} | |
}); | |
return filtered; | |
}, | |
findNextWhere = helpers.findNextWhere = function(arrayToSearch, filterCallback, startIndex){ | |
// Default to start of the array | |
if (!startIndex){ | |
startIndex = -1; | |
} | |
for (var i = startIndex + 1; i < arrayToSearch.length; i++) { | |
var currentItem = arrayToSearch[i]; | |
if (filterCallback(currentItem)){ | |
return currentItem; | |
} | |
}; | |
}, | |
findPreviousWhere = helpers.findPreviousWhere = function(arrayToSearch, filterCallback, startIndex){ | |
// Default to end of the array | |
if (!startIndex){ | |
startIndex = arrayToSearch.length; | |
} | |
for (var i = startIndex - 1; i >= 0; i--) { | |
var currentItem = arrayToSearch[i]; | |
if (filterCallback(currentItem)){ | |
return currentItem; | |
} | |
}; | |
}, | |
inherits = helpers.inherits = function(extensions){ | |
//Basic javascript inheritance based on the model created in Backbone.js | |
var parent = this; | |
var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); }; | |
var Surrogate = function(){ this.constructor = ChartElement;}; | |
Surrogate.prototype = parent.prototype; | |
ChartElement.prototype = new Surrogate(); | |
ChartElement.extend = inherits; | |
if (extensions) extend(ChartElement.prototype, extensions); | |
ChartElement.__super__ = parent.prototype; | |
return ChartElement; | |
}, | |
noop = helpers.noop = function(){}, | |
uid = helpers.uid = (function(){ | |
var id=0; | |
return function(){ | |
return "chart-" + id++; | |
}; | |
})(), | |
warn = helpers.warn = function(str){ | |
//Method for warning of errors | |
if (window.console && typeof window.console.warn == "function") console.warn(str); | |
}, | |
amd = helpers.amd = (typeof root.define == 'function' && root.define.amd), | |
//-- Math methods | |
isNumber = helpers.isNumber = function(n){ | |
return !isNaN(parseFloat(n)) && isFinite(n); | |
}, | |
max = helpers.max = function(array){ | |
return Math.max.apply( Math, array ); | |
}, | |
min = helpers.min = function(array){ | |
return Math.min.apply( Math, array ); | |
}, | |
cap = helpers.cap = function(valueToCap,maxValue,minValue){ | |
if(isNumber(maxValue)) { | |
if( valueToCap > maxValue ) { | |
return maxValue; | |
} | |
} | |
else if(isNumber(minValue)){ | |
if ( valueToCap < minValue ){ | |
return minValue; | |
} | |
} | |
return valueToCap; | |
}, | |
getDecimalPlaces = helpers.getDecimalPlaces = function(num){ | |
if (num%1!==0 && isNumber(num)){ | |
return num.toString().split(".")[1].length; | |
} | |
else { | |
return 0; | |
} | |
}, | |
toRadians = helpers.radians = function(degrees){ | |
return degrees * (Math.PI/180); | |
}, | |
// Gets the angle from vertical upright to the point about a centre. | |
getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){ | |
var distanceFromXCenter = anglePoint.x - centrePoint.x, | |
distanceFromYCenter = anglePoint.y - centrePoint.y, | |
radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); | |
var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter); | |
//If the segment is in the top left quadrant, we need to add another rotation to the angle | |
if (distanceFromXCenter < 0 && distanceFromYCenter < 0){ | |
angle += Math.PI*2; | |
} | |
return { | |
angle: angle, | |
distance: radialDistanceFromCenter | |
}; | |
}, | |
aliasPixel = helpers.aliasPixel = function(pixelWidth){ | |
return (pixelWidth % 2 === 0) ? 0 : 0.5; | |
}, | |
splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){ | |
//Props to Rob Spencer at scaled innovation for his post on splining between points | |
//http://scaledinnovation.com/analytics/splines/aboutSplines.html | |
var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)), | |
d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)), | |
fa=t*d01/(d01+d12),// scaling factor for triangle Ta | |
fb=t*d12/(d01+d12); | |
return { | |
inner : { | |
x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x), | |
y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y) | |
}, | |
outer : { | |
x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x), | |
y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y) | |
} | |
}; | |
}, | |
calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){ | |
return Math.floor(Math.log(val) / Math.LN10); | |
}, | |
calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){ | |
//Set a minimum step of two - a point at the top of the graph, and a point at the base | |
var minSteps = 2, | |
maxSteps = Math.floor(drawingSize/(textSize * 1.5)), | |
skipFitting = (minSteps >= maxSteps); | |
var maxValue = max(valuesArray), | |
minValue = min(valuesArray); | |
// We need some degree of seperation here to calculate the scales if all the values are the same | |
// Adding/minusing 0.5 will give us a range of 1. | |
if (maxValue === minValue){ | |
maxValue += 0.5; | |
// So we don't end up with a graph with a negative start value if we've said always start from zero | |
if (minValue >= 0.5 && !startFromZero){ | |
minValue -= 0.5; | |
} | |
else{ | |
// Make up a whole number above the values | |
maxValue += 0.5; | |
} | |
} | |
var valueRange = Math.abs(maxValue - minValue), | |
rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange), | |
graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), | |
graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude), | |
graphRange = graphMax - graphMin, | |
stepValue = Math.pow(10, rangeOrderOfMagnitude), | |
numberOfSteps = Math.round(graphRange / stepValue); | |
//If we have more space on the graph we'll use it to give more definition to the data | |
while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) { | |
if(numberOfSteps > maxSteps){ | |
stepValue *=2; | |
numberOfSteps = Math.round(graphRange/stepValue); | |
// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps. | |
if (numberOfSteps % 1 !== 0){ | |
skipFitting = true; | |
} | |
} | |
//We can fit in double the amount of scale points on the scale | |
else{ | |
//If user has declared ints only, and the step value isn't a decimal | |
if (integersOnly && rangeOrderOfMagnitude >= 0){ | |
//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float | |
if(stepValue/2 % 1 === 0){ | |
stepValue /=2; | |
numberOfSteps = Math.round(graphRange/stepValue); | |
} | |
//If it would make it a float break out of the loop | |
else{ | |
break; | |
} | |
} | |
//If the scale doesn't have to be an int, make the scale more granular anyway. | |
else{ | |
stepValue /=2; | |
numberOfSteps = Math.round(graphRange/stepValue); | |
} | |
} | |
} | |
if (skipFitting){ | |
numberOfSteps = minSteps; | |
stepValue = graphRange / numberOfSteps; | |
} | |
return { | |
steps : numberOfSteps, | |
stepValue : stepValue, | |
min : graphMin, | |
max : graphMin + (numberOfSteps * stepValue) | |
}; | |
}, | |
/* jshint ignore:start */ | |
// Blows up jshint errors based on the new Function constructor | |
//Templating methods | |
//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/ | |
template = helpers.template = function(templateString, valuesObject){ | |
// If templateString is function rather than string-template - call the function for valuesObject | |
if(templateString instanceof Function){ | |
return templateString(valuesObject); | |
} | |
var cache = {}; | |
function tmpl(str, data){ | |
// Figure out if we're getting a template, or if we need to | |
// load the template - and be sure to cache the result. | |
var fn = !/\W/.test(str) ? | |
cache[str] = cache[str] : | |
// Generate a reusable function that will serve as a template | |
// generator (and which will be cached). | |
new Function("obj", | |
"var p=[],print=function(){p.push.apply(p,arguments);};" + | |
// Introduce the data as local variables using with(){} | |
"with(obj){p.push('" + | |
// Convert the template into pure JavaScript | |
str | |
.replace(/[\r\t\n]/g, " ") | |
.split("<%").join("\t") | |
.replace(/((^|%>)[^\t]*)'/g, "$1\r") | |
.replace(/\t=(.*?)%>/g, "',$1,'") | |
.split("\t").join("');") | |
.split("%>").join("p.push('") | |
.split("\r").join("\\'") + | |
"');}return p.join('');" | |
); | |
// Provide some basic currying to the user | |
return data ? fn( data ) : fn; | |
} | |
return tmpl(templateString,valuesObject); | |
}, | |
/* jshint ignore:end */ | |
generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){ | |
var labelsArray = new Array(numberOfSteps); | |
if (labelTemplateString){ | |
each(labelsArray,function(val,index){ | |
labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))}); | |
}); | |
} | |
return labelsArray; | |
}, | |
//--Animation methods | |
//Easing functions adapted from Robert Penner's easing equations | |
//http://www.robertpenner.com/easing/ | |
easingEffects = helpers.easingEffects = { | |
linear: function (t) { | |
return t; | |
}, | |
easeInQuad: function (t) { | |
return t * t; | |
}, | |
easeOutQuad: function (t) { | |
return -1 * t * (t - 2); | |
}, | |
easeInOutQuad: function (t) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t; | |
return -1 / 2 * ((--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) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t; | |
return 1 / 2 * ((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) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t; | |
return -1 / 2 * ((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) { | |
if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t; | |
return 1 / 2 * ((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 -1 / 2 * (Math.cos(Math.PI * t / 1) - 1); | |
}, | |
easeInExpo: function (t) { | |
return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1)); | |
}, | |
easeOutExpo: function (t) { | |
return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1); | |
}, | |
easeInOutExpo: function (t) { | |
if (t === 0) return 0; | |
if (t === 1) return 1; | |
if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1)); | |
return 1 / 2 * (-Math.pow(2, -10 * --t) + 2); | |
}, | |
easeInCirc: function (t) { | |
if (t >= 1) return t; | |
return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1); | |
}, | |
easeOutCirc: function (t) { | |
return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t); | |
}, | |
easeInOutCirc: function (t) { | |
if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1); | |
return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1); | |
}, | |
easeInElastic: function (t) { | |
var s = 1.70158; | |
var p = 0; | |
var a = 1; | |
if (t === 0) return 0; | |
if ((t /= 1) == 1) return 1; | |
if (!p) p = 1 * 0.3; | |
if (a < Math.abs(1)) { | |
a = 1; | |
s = p / 4; | |
} else s = p / (2 * Math.PI) * Math.asin(1 / a); | |
return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); | |
}, | |
easeOutElastic: function (t) { | |
var s = 1.70158; | |
var p = 0; | |
var a = 1; | |
if (t === 0) return 0; | |
if ((t /= 1) == 1) return 1; | |
if (!p) p = 1 * 0.3; | |
if (a < Math.abs(1)) { | |
a = 1; | |
s = p / 4; | |
} else s = p / (2 * Math.PI) * Math.asin(1 / a); | |
return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1; | |
}, | |
easeInOutElastic: function (t) { | |
var s = 1.70158; | |
var p = 0; | |
var a = 1; | |
if (t === 0) return 0; | |
if ((t /= 1 / 2) == 2) return 1; | |
if (!p) p = 1 * (0.3 * 1.5); | |
if (a < Math.abs(1)) { | |
a = 1; | |
s = p / 4; | |
} else s = p / (2 * Math.PI) * Math.asin(1 / a); | |
if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p)); | |
return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1; | |
}, | |
easeInBack: function (t) { | |
var s = 1.70158; | |
return 1 * (t /= 1) * t * ((s + 1) * t - s); | |
}, | |
easeOutBack: function (t) { | |
var s = 1.70158; | |
return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1); | |
}, | |
easeInOutBack: function (t) { | |
var s = 1.70158; | |
if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s)); | |
return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); | |
}, | |
easeInBounce: function (t) { | |
return 1 - easingEffects.easeOutBounce(1 - t); | |
}, | |
easeOutBounce: function (t) { | |
if ((t /= 1) < (1 / 2.75)) { | |
return 1 * (7.5625 * t * t); | |
} else if (t < (2 / 2.75)) { | |
return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75); | |
} else if (t < (2.5 / 2.75)) { | |
return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375); | |
} else { | |
return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375); | |
} | |
}, | |
easeInOutBounce: function (t) { | |
if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5; | |
return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5; | |
} | |
}, | |
//Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
requestAnimFrame = helpers.requestAnimFrame = (function(){ | |
return window.requestAnimationFrame || | |
window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.oRequestAnimationFrame || | |
window.msRequestAnimationFrame || | |
function(callback) { | |
return window.setTimeout(callback, 1000 / 60); | |
}; | |
})(), | |
cancelAnimFrame = helpers.cancelAnimFrame = (function(){ | |
return window.cancelAnimationFrame || | |
window.webkitCancelAnimationFrame || | |
window.mozCancelAnimationFrame || | |
window.oCancelAnimationFrame || | |
window.msCancelAnimationFrame || | |
function(callback) { | |
return window.clearTimeout(callback, 1000 / 60); | |
}; | |
})(), | |
animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){ | |
var currentStep = 0, | |
easingFunction = easingEffects[easingString] || easingEffects.linear; | |
var animationFrame = function(){ | |
currentStep++; | |
var stepDecimal = currentStep/totalSteps; | |
var easeDecimal = easingFunction(stepDecimal); | |
callback.call(chartInstance,easeDecimal,stepDecimal, currentStep); | |
onProgress.call(chartInstance,easeDecimal,stepDecimal); | |
if (currentStep < totalSteps){ | |
chartInstance.animationFrame = requestAnimFrame(animationFrame); | |
} else{ | |
onComplete.apply(chartInstance); | |
} | |
}; | |
requestAnimFrame(animationFrame); | |
}, | |
//-- DOM methods | |
getRelativePosition = helpers.getRelativePosition = function(evt){ | |
var mouseX, mouseY; | |
var e = evt.originalEvent || evt, | |
canvas = evt.currentTarget || evt.srcElement, | |
boundingRect = canvas.getBoundingClientRect(); | |
if (e.touches){ | |
mouseX = e.touches[0].clientX - boundingRect.left; | |
mouseY = e.touches[0].clientY - boundingRect.top; | |
} | |
else{ | |
mouseX = e.clientX - boundingRect.left; | |
mouseY = e.clientY - boundingRect.top; | |
} | |
return { | |
x : mouseX, | |
y : mouseY | |
}; | |
}, | |
addEvent = helpers.addEvent = function(node,eventType,method){ | |
if (node.addEventListener){ | |
node.addEventListener(eventType,method); | |
} else if (node.attachEvent){ | |
node.attachEvent("on"+eventType, method); | |
} else { | |
node["on"+eventType] = method; | |
} | |
}, | |
removeEvent = helpers.removeEvent = function(node, eventType, handler){ | |
if (node.removeEventListener){ | |
node.removeEventListener(eventType, handler, false); | |
} else if (node.detachEvent){ | |
node.detachEvent("on"+eventType,handler); | |
} else{ | |
node["on" + eventType] = noop; | |
} | |
}, | |
bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){ | |
// Create the events object if it's not already present | |
if (!chartInstance.events) chartInstance.events = {}; | |
each(arrayOfEvents,function(eventName){ | |
chartInstance.events[eventName] = function(){ | |
handler.apply(chartInstance, arguments); | |
}; | |
addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]); | |
}); | |
}, | |
unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) { | |
each(arrayOfEvents, function(handler,eventName){ | |
removeEvent(chartInstance.chart.canvas, eventName, handler); | |
}); | |
}, | |
getMaximumWidth = helpers.getMaximumWidth = function(domNode){ | |
var container = domNode.parentNode; | |
// TODO = check cross browser stuff with this. | |
return container.clientWidth; | |
}, | |
getMaximumHeight = helpers.getMaximumHeight = function(domNode){ | |
var container = domNode.parentNode; | |
// TODO = check cross browser stuff with this. | |
return container.clientHeight; | |
}, | |
getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support | |
retinaScale = helpers.retinaScale = function(chart){ | |
var ctx = chart.ctx, | |
width = chart.canvas.width, | |
height = chart.canvas.height; | |
if (window.devicePixelRatio) { | |
ctx.canvas.style.width = width + "px"; | |
ctx.canvas.style.height = height + "px"; | |
ctx.canvas.height = height * window.devicePixelRatio; | |
ctx.canvas.width = width * window.devicePixelRatio; | |
ctx.scale(window.devicePixelRatio, window.devicePixelRatio); | |
} | |
}, | |
//-- Canvas methods | |
clear = helpers.clear = function(chart){ | |
chart.ctx.clearRect(0,0,chart.width,chart.height); | |
}, | |
fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){ | |
return fontStyle + " " + pixelSize+"px " + fontFamily; | |
}, | |
longestText = helpers.longestText = function(ctx,font,arrayOfStrings){ | |
ctx.font = font; | |
var longest = 0; | |
each(arrayOfStrings,function(string){ | |
var textWidth = ctx.measureText(string).width; | |
longest = (textWidth > longest) ? textWidth : longest; | |
}); | |
return longest; | |
}, | |
drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){ | |
ctx.beginPath(); | |
ctx.moveTo(x + radius, y); | |
ctx.lineTo(x + width - radius, y); | |
ctx.quadraticCurveTo(x + width, y, x + width, y + radius); | |
ctx.lineTo(x + width, y + height - radius); | |
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); | |
ctx.lineTo(x + radius, y + height); | |
ctx.quadraticCurveTo(x, y + height, x, y + height - radius); | |
ctx.lineTo(x, y + radius); | |
ctx.quadraticCurveTo(x, y, x + radius, y); | |
ctx.closePath(); | |
}; | |
//Store a reference to each instance - allowing us to globally resize chart instances on window resize. | |
//Destroy method on the chart will remove the instance of the chart from this reference. | |
Chart.instances = {}; | |
Chart.Type = function(data,options,chart){ | |
this.options = options; | |
this.chart = chart; | |
this.id = uid(); | |
//Add the chart instance to the global namespace | |
Chart.instances[this.id] = this; | |
// Initialize is always called when a chart type is created | |
// By default it is a no op, but it should be extended | |
if (options.responsive){ | |
this.resize(); | |
} | |
this.initialize.call(this,data); | |
}; | |
//Core methods that'll be a part of every chart type | |
extend(Chart.Type.prototype,{ | |
initialize : function(){return this;}, | |
clear : function(){ | |
clear(this.chart); | |
return this; | |
}, | |
stop : function(){ | |
// Stops any current animation loop occuring | |
helpers.cancelAnimFrame.call(root, this.animationFrame); | |
return this; | |
}, | |
resize : function(callback){ | |
this.stop(); | |
var canvas = this.chart.canvas, | |
newWidth = getMaximumWidth(this.chart.canvas), | |
newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas); | |
canvas.width = this.chart.width = newWidth; | |
canvas.height = this.chart.height = newHeight; | |
retinaScale(this.chart); | |
if (typeof callback === "function"){ | |
callback.apply(this, Array.prototype.slice.call(arguments, 1)); | |
} | |
return this; | |
}, | |
reflow : noop, | |
render : function(reflow){ | |
if (reflow){ | |
this.reflow(); | |
} | |
if (this.options.animation && !reflow){ | |
helpers.animationLoop( | |
this.draw, | |
this.options.animationSteps, | |
this.options.animationEasing, | |
this.options.onAnimationProgress, | |
this.options.onAnimationComplete, | |
this | |
); | |
} | |
else{ | |
this.draw(); | |
this.options.onAnimationComplete.call(this); | |
} | |
return this; | |
}, | |
generateLegend : function(){ | |
return template(this.options.legendTemplate,this); | |
}, | |
destroy : function(){ | |
this.clear(); | |
unbindEvents(this, this.events); | |
delete Chart.instances[this.id]; | |
}, | |
showTooltip : function(ChartElements, forceRedraw){ | |
// Only redraw the chart if we've actually changed what we're hovering on. | |
if (typeof this.activeElements === 'undefined') this.activeElements = []; | |
var isChanged = (function(Elements){ | |
var changed = false; | |
if (Elements.length !== this.activeElements.length){ | |
changed = true; | |
return changed; | |
} | |
each(Elements, function(element, index){ | |
if (element !== this.activeElements[index]){ | |
changed = true; | |
} | |
}, this); | |
return changed; | |
}).call(this, ChartElements); | |
if (!isChanged && !forceRedraw){ | |
return; | |
} | |
else{ | |
this.activeElements = ChartElements; | |
} | |
this.draw(); | |
if (ChartElements.length > 0){ | |
// If we have multiple datasets, show a MultiTooltip for all of the data points at that index | |
if (this.datasets && this.datasets.length > 1) { | |
var dataArray, | |
dataIndex; | |
for (var i = this.datasets.length - 1; i >= 0; i--) { | |
dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments; | |
dataIndex = indexOf(dataArray, ChartElements[0]); | |
if (dataIndex !== -1){ | |
break; | |
} | |
} | |
var tooltipLabels = [], | |
tooltipColors = [], | |
medianPosition = (function(index) { | |
// Get all the points at that particular index | |
var Elements = [], | |
dataCollection, | |
xPositions = [], | |
yPositions = [], | |
xMax, | |
yMax, | |
xMin, | |
yMin; | |
helpers.each(this.datasets, function(dataset){ | |
dataCollection = dataset.points || dataset.bars || dataset.segments; | |
if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){ | |
Elements.push(dataCollection[dataIndex]); | |
} | |
}); | |
helpers.each(Elements, function(element) { | |
xPositions.push(element.x); | |
yPositions.push(element.y); | |
//Include any colour information about the element | |
tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element)); | |
tooltipColors.push({ | |
fill: element._saved.fillColor || element.fillColor, | |
stroke: element._saved.strokeColor || element.strokeColor | |
}); | |
}, this); | |
yMin = min(yPositions); | |
yMax = max(yPositions); | |
xMin = min(xPositions); | |
xMax = max(xPositions); | |
return { | |
x: (xMin > this.chart.width/2) ? xMin : xMax, | |
y: (yMin + yMax)/2 | |
}; | |
}).call(this, dataIndex); | |
new Chart.MultiTooltip({ | |
x: medianPosition.x, | |
y: medianPosition.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: tooltipLabels, | |
legendColors: tooltipColors, | |
legendColorBackground : this.options.multiTooltipKeyBackground, | |
title: ChartElements[0].label, | |
chart: this.chart, | |
ctx: this.chart.ctx | |
}).draw(); | |
} else { | |
each(ChartElements, function(Element) { | |
var tooltipPosition = Element.tooltipPosition(); | |
new Chart.Tooltip({ | |
x: Math.round(tooltipPosition.x), | |
y: Math.round(tooltipPosition.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: template(this.options.tooltipTemplate, Element), | |
chart: this.chart | |
}).draw(); | |
}, this); | |
} | |
} | |
return this; | |
}, | |
toBase64Image : function(){ | |
return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments); | |
} | |
}); | |
Chart.Type.extend = function(extensions){ | |
var parent = this; | |
var ChartType = function(){ | |
return parent.apply(this,arguments); | |
}; | |
//Copy the prototype object of the this class | |
ChartType.prototype = clone(parent.prototype); | |
//Now overwrite some of the properties in the base class with the new extensions | |
extend(ChartType.prototype, extensions); | |
ChartType.extend = Chart.Type.extend; | |
if (extensions.name || parent.prototype.name){ | |
var chartName = extensions.name || parent.prototype.name; | |
//Assign any potential default values of the new chart type | |
//If none are defined, we'll use a clone of the chart type this is being extended from. | |
//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart | |
//doesn't define some defaults of their own. | |
var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {}; | |
Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults); | |
Chart.types[chartName] = ChartType; | |
//Register this new chart type in the Chart prototype | |
Chart.prototype[chartName] = function(data,options){ | |
var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {}); | |
return new ChartType(data,config,this); | |
}; | |
} else{ | |
warn("Name not provided for this chart, so it hasn't been registered"); | |
} | |
return parent; | |
}; | |
Chart.Element = function(configuration){ | |
extend(this,configuration); | |
this.initialize.apply(this,arguments); | |
this.save(); | |
}; | |
extend(Chart.Element.prototype,{ | |
initialize : function(){}, | |
restore : function(props){ | |
if (!props){ | |
extend(this,this._saved); | |
} else { | |
each(props,function(key){ | |
this[key] = this._saved[key]; | |
},this); | |
} | |
return this; | |
}, | |
save : function(){ | |
this._saved = clone(this); | |
delete this._saved._saved; | |
return this; | |
}, | |
update : function(newProps){ | |
each(newProps,function(value,key){ | |
this._saved[key] = this[key]; | |
this[key] = value; | |
},this); | |
return this; | |
}, | |
transition : function(props,ease){ | |
each(props,function(value,key){ | |
this[key] = ((value - this._saved[key]) * ease) + this._saved[key]; | |
},this); | |
return this; | |
}, | |
tooltipPosition : function(){ | |
return { | |
x : this.x, | |
y : this.y | |
}; | |
}, | |
hasValue: function(){ | |
return isNumber(this.value); | |
} | |
}); | |
Chart.Element.extend = inherits; | |
Chart.Point = Chart.Element.extend({ | |
display: true, | |
inRange: function(chartX,chartY){ | |
var hitDetectionRange = this.hitDetectionRadius + this.radius; | |
return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2)); | |
}, | |
draw : function(){ | |
if (this.display){ | |
var ctx = this.ctx; | |
ctx.beginPath(); | |
ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2); | |
ctx.closePath(); | |
ctx.strokeStyle = this.strokeColor; | |
ctx.lineWidth = this.strokeWidth; | |
ctx.fillStyle = this.fillColor; | |
ctx.fill(); | |
ctx.stroke(); | |
} | |
//Quick debug for bezier curve splining | |
//Highlights control points and the line between them. | |
//Handy for dev - stripped in the min version. | |
// ctx.save(); | |
// ctx.fillStyle = "black"; | |
// ctx.strokeStyle = "black" | |
// ctx.beginPath(); | |
// ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2); | |
// ctx.fill(); | |
// ctx.beginPath(); | |
// ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2); | |
// ctx.fill(); | |
// ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y); | |
// ctx.lineTo(this.x, this.y); | |
// ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y); | |
// ctx.stroke(); | |
// ctx.restore(); | |
} | |
}); | |
Chart.Arc = Chart.Element.extend({ | |
inRange : function(chartX,chartY){ | |
var pointRelativePosition = helpers.getAngleFromPoint(this, { | |
x: chartX, | |
y: chartY | |
}); | |
//Check if within the range of the open/close angle | |
var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle), | |
withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius); | |
return (betweenAngles && withinRadius); | |
//Ensure within the outside of the arc centre, but inside arc outer | |
}, | |
tooltipPosition : function(){ | |
var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2), | |
rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius; | |
return { | |
x : this.x + (Math.cos(centreAngle) * rangeFromCentre), | |
y : this.y + (Math.sin(centreAngle) * rangeFromCentre) | |
}; | |
}, | |
draw : function(animationPercent){ | |
var easingDecimal = animationPercent || 1; | |
var ctx = this.ctx; | |
ctx.beginPath(); | |
ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle); | |
ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true); | |
ctx.closePath(); | |
ctx.strokeStyle = this.strokeColor; | |
ctx.lineWidth = this.strokeWidth; | |
ctx.fillStyle = this.fillColor; | |
ctx.fill(); | |
ctx.lineJoin = 'bevel'; | |
if (this.showStroke){ | |
ctx.stroke(); | |
} | |
} | |
}); | |
Chart.Rectangle = Chart.Element.extend({ | |
draw : function(){ | |
var ctx = this.ctx, | |
halfWidth = this.width/2, | |
leftX = this.x - halfWidth, | |
rightX = this.x + halfWidth, | |
top = this.base - (this.base - this.y), | |
halfStroke = this.strokeWidth / 2; | |
// Canvas doesn't allow us to stroke inside the width so we can | |
// adjust the sizes to fit if we're setting a stroke on the line | |
if (this.showStroke){ | |
leftX += halfStroke; | |
rightX -= halfStroke; | |
top += halfStroke; | |
} | |
ctx.beginPath(); | |
ctx.fillStyle = this.fillColor; | |
ctx.strokeStyle = this.strokeColor; | |
ctx.lineWidth = this.strokeWidth; | |
// It'd be nice to keep this class totally generic to any rectangle | |
// and simply specify which border to miss out. | |
ctx.moveTo(leftX, this.base); | |
ctx.lineTo(leftX, top); | |
ctx.lineTo(rightX, top); | |
ctx.lineTo(rightX, this.base); | |
ctx.fill(); | |
if (this.showStroke){ | |
ctx.stroke(); | |
} | |
}, | |
height : function(){ | |
return this.base - this.y; | |
}, | |
inRange : function(chartX,chartY){ | |
return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base); | |
} | |
}); | |
Chart.Tooltip = Chart.Element.extend({ | |
draw : function(){ | |
var ctx = this.chart.ctx; | |
ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); | |
this.xAlign = "center"; | |
this.yAlign = "above"; | |
//Distance between the actual element.y position and the start of the tooltip caret | |
var caretPadding = 2; | |
var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding, | |
tooltipRectHeight = this.fontSize + 2*this.yPadding, | |
tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding; | |
if (this.x + tooltipWidth/2 >this.chart.width){ | |
this.xAlign = "left"; | |
} else if (this.x - tooltipWidth/2 < 0){ | |
this.xAlign = "right"; | |
} | |
if (this.y - tooltipHeight < 0){ | |
this.yAlign = "below"; | |
} | |
var tooltipX = this.x - tooltipWidth/2, | |
tooltipY = this.y - tooltipHeight; | |
ctx.fillStyle = this.fillColor; | |
switch(this.yAlign) | |
{ | |
case "above": | |
//Draw a caret above the x/y | |
ctx.beginPath(); | |
ctx.moveTo(this.x,this.y - caretPadding); | |
ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight)); | |
ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight)); | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
case "below": | |
tooltipY = this.y + caretPadding + this.caretHeight; | |
//Draw a caret below the x/y | |
ctx.beginPath(); | |
ctx.moveTo(this.x, this.y + caretPadding); | |
ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight); | |
ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight); | |
ctx.closePath(); | |
ctx.fill(); | |
break; | |
} | |
switch(this.xAlign) | |
{ | |
case "left": | |
tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight); | |
break; | |
case "right": | |
tooltipX = this.x - (this.cornerRadius + this.caretHeight); | |
break; | |
} | |
drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius); | |
ctx.fill(); | |
ctx.fillStyle = this.textColor; | |
ctx.textAlign = "center"; | |
ctx.textBaseline = "middle"; | |
ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2); | |
} | |
}); | |
Chart.MultiTooltip = Chart.Element.extend({ | |
initialize : function(){ | |
this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); | |
this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily); | |
this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5; | |
this.ctx.font = this.titleFont; | |
var titleWidth = this.ctx.measureText(this.title).width, | |
//Label has a legend square as well so account for this. | |
labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3, | |
longestTextWidth = max([labelWidth,titleWidth]); | |
this.width = longestTextWidth + (this.xPadding*2); | |
var halfHeight = this.height/2; | |
//Check to ensure the height will fit on the canvas | |
//The three is to buffer form the very | |
if (this.y - halfHeight < 0 ){ | |
this.y = halfHeight; | |
} else if (this.y + halfHeight > this.chart.height){ | |
this.y = this.chart.height - halfHeight; | |
} | |
//Decide whether to align left or right based on position on canvas | |
if (this.x > this.chart.width/2){ | |
this.x -= this.xOffset + this.width; | |
} else { | |
this.x += this.xOffset; | |
} | |
}, | |
getLineHeight : function(index){ | |
var baseLineHeight = this.y - (this.height/2) + this.yPadding, | |
afterTitleIndex = index-1; | |
//If the index is zero, we're getting the title | |
if (index === 0){ | |
return baseLineHeight + this.titleFontSize/2; | |
} else{ | |
return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5; | |
} | |
}, | |
draw : function(){ | |
drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius); | |
var ctx = this.ctx; | |
ctx.fillStyle = this.fillColor; | |
ctx.fill(); | |
ctx.closePath(); | |
ctx.textAlign = "left"; | |
ctx.textBaseline = "middle"; | |
ctx.fillStyle = this.titleTextColor; | |
ctx.font = this.titleFont; | |
ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0)); | |
ctx.font = this.font; | |
helpers.each(this.labels,function(label,index){ | |
ctx.fillStyle = this.textColor; | |
ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1)); | |
//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas) | |
//ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); | |
//Instead we'll make a white filled block to put the legendColour palette over. | |
ctx.fillStyle = this.legendColorBackground; | |
ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); | |
ctx.fillStyle = this.legendColors[index].fill; | |
ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize); | |
},this); | |
} | |
}); | |
Chart.Scale = Chart.Element.extend({ | |
initialize : function(){ | |
this.fit(); | |
}, | |
buildYLabels : function(){ | |
this.yLabels = []; | |
var stepDecimalPlaces = getDecimalPlaces(this.stepValue); | |
for (var i=0; i<=this.steps; i++){ | |
this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); | |
} | |
this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0; | |
}, | |
addXLabel : function(label){ | |
this.xLabels.push(label); | |
this.valuesCount++; | |
this.fit(); | |
}, | |
removeXLabel : function(){ | |
this.xLabels.shift(); | |
this.valuesCount--; | |
this.fit(); | |
}, | |
// Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use | |
fit: function(){ | |
// First we need the width of the yLabels, assuming the xLabels aren't rotated | |
// To do that we need the base line at the top and base of the chart, assuming there is no x label rotation | |
this.startPoint = (this.display) ? this.fontSize : 0; | |
this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels | |
// Apply padding settings to the start and end point. | |
this.startPoint += this.padding; | |
this.endPoint -= this.padding; | |
// Cache the starting height, so can determine if we need to recalculate the scale yAxis | |
var cachedHeight = this.endPoint - this.startPoint, | |
cachedYLabelWidth; | |
// Build the current yLabels so we have an idea of what size they'll be to start | |
/* | |
* This sets what is returned from calculateScaleRange as static properties of this class: | |
* | |
this.steps; | |
this.stepValue; | |
this.min; | |
this.max; | |
* | |
*/ | |
this.calculateYRange(cachedHeight); | |
// With these properties set we can now build the array of yLabels | |
// and also the width of the largest yLabel | |
this.buildYLabels(); | |
this.calculateXLabelRotation(); | |
while((cachedHeight > this.endPoint - this.startPoint)){ | |
cachedHeight = this.endPoint - this.startPoint; | |
cachedYLabelWidth = this.yLabelWidth; | |
this.calculateYRange(cachedHeight); | |
this.buildYLabels(); | |
// Only go through the xLabel loop again if the yLabel width has changed | |
if (cachedYLabelWidth < this.yLabelWidth){ | |
this.calculateXLabelRotation(); | |
} | |
} | |
}, | |
calculateXLabelRotation : function(){ | |
//Get the width of each grid by calculating the difference | |
//between x offsets between 0 and 1. | |
this.ctx.font = this.font; | |
var firstWidth = this.ctx.measureText(this.xLabels[0]).width, | |
lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width, | |
firstRotated, | |
lastRotated; | |
this.xScalePaddingRight = lastWidth/2 + 3; | |
this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10; | |
this.xLabelRotation = 0; | |
if (this.display){ | |
var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels), | |
cosRotation, | |
firstRotatedWidth; | |
this.xLabelWidth = originalLabelWidth; | |
//Allow 3 pixels x2 padding either side for label readability | |
var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6; | |
//Max label rotate should be 90 - also act as a loop counter | |
while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){ | |
cosRotation = Math.cos(toRadians(this.xLabelRotation)); | |
firstRotated = cosRotation * firstWidth; | |
lastRotated = cosRotation * lastWidth; | |
// We're right aligning the text now. | |
if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){ | |
this.xScalePaddingLeft = firstRotated + this.fontSize / 2; | |
} | |
this.xScalePaddingRight = this.fontSize/2; | |
this.xLabelRotation++; | |
this.xLabelWidth = cosRotation * originalLabelWidth; | |
} | |
if (this.xLabelRotation > 0){ | |
this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3; | |
} | |
} | |
else{ | |
this.xLabelWidth = 0; | |
this.xScalePaddingRight = this.padding; | |
this.xScalePaddingLeft = this.padding; | |
} | |
}, | |
// Needs to be overidden in each Chart type | |
// Otherwise we need to pass all the data into the scale class | |
calculateYRange: noop, | |
drawingArea: function(){ | |
return this.startPoint - this.endPoint; | |
}, | |
calculateY : function(value){ | |
var scalingFactor = this.drawingArea() / (this.min - this.max); | |
return this.endPoint - (scalingFactor * (value - this.min)); | |
}, | |
calculateX : function(index){ | |
var isRotated = (this.xLabelRotation > 0), | |
// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding, | |
innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight), | |
valueWidth = innerWidth/(this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), | |
valueOffset = (valueWidth * index) + this.xScalePaddingLeft; | |
if (this.offsetGridLines){ | |
valueOffset += (valueWidth/2); | |
} | |
return Math.round(valueOffset); | |
}, | |
update : function(newProps){ | |
helpers.extend(this, newProps); | |
this.fit(); | |
}, | |
draw : function(){ | |
var ctx = this.ctx, | |
yLabelGap = (this.endPoint - this.startPoint) / this.steps, | |
xStart = Math.round(this.xScalePaddingLeft); | |
if (this.display){ | |
ctx.fillStyle = this.textColor; | |
ctx.font = this.font; | |
each(this.yLabels,function(labelString,index){ | |
var yLabelCenter = this.endPoint - (yLabelGap * index), | |
linePositionY = Math.round(yLabelCenter); | |
ctx.textAlign = "right"; | |
ctx.textBaseline = "middle"; | |
if (this.showLabels){ | |
ctx.fillText(labelString,xStart - 10,yLabelCenter); | |
} | |
ctx.beginPath(); | |
if (index > 0){ | |
// This is a grid line in the centre, so drop that | |
ctx.lineWidth = this.gridLineWidth; | |
ctx.strokeStyle = this.gridLineColor; | |
} else { | |
// This is the first line on the scale | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
} | |
linePositionY += helpers.aliasPixel(ctx.lineWidth); | |
ctx.moveTo(xStart, linePositionY); | |
ctx.lineTo(this.width, linePositionY); | |
ctx.stroke(); | |
ctx.closePath(); | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
ctx.beginPath(); | |
ctx.moveTo(xStart - 5, linePositionY); | |
ctx.lineTo(xStart, linePositionY); | |
ctx.stroke(); | |
ctx.closePath(); | |
},this); | |
each(this.xLabels,function(label,index){ | |
var xPos = this.calculateX(index) + aliasPixel(this.lineWidth), | |
// Check to see if line/bar here and decide where to place the line | |
linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth), | |
isRotated = (this.xLabelRotation > 0); | |
ctx.beginPath(); | |
if (index > 0){ | |
// This is a grid line in the centre, so drop that | |
ctx.lineWidth = this.gridLineWidth; | |
ctx.strokeStyle = this.gridLineColor; | |
} else { | |
// This is the first line on the scale | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
} | |
ctx.moveTo(linePos,this.endPoint); | |
ctx.lineTo(linePos,this.startPoint - 3); | |
ctx.stroke(); | |
ctx.closePath(); | |
ctx.lineWidth = this.lineWidth; | |
ctx.strokeStyle = this.lineColor; | |
// Small lines at the bottom of the base grid line | |
ctx.beginPath(); | |
ctx.moveTo(linePos,this.endPoint); | |
ctx.lineTo(linePos,this.endPoint + 5); | |
ctx.stroke(); | |
ctx.closePath(); | |
ctx.save(); | |
ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8); | |
ctx.rotate(toRadians(this.xLabelRotation)*-1); | |
ctx.font = this.font; | |
ctx.textAlign = (isRotated) ? "right" : "center"; | |
ctx.textBaseline = (isRotated) ? "middle" : "top"; | |
ctx.fillText(label, 0, 0); | |
ctx.restore(); | |
},this); | |
} | |
} | |
}); | |
Chart.RadialScale = Chart.Element.extend({ | |
initialize: function(){ | |
this.size = min([this.height, this.width]); | |
this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); | |
}, | |
calculateCenterOffset: function(value){ | |
// Take into account half font size + the yPadding of the top value | |
var scalingFactor = this.drawingArea / (this.max - this.min); | |
return (value - this.min) * scalingFactor; | |
}, | |
update : function(){ | |
if (!this.lineArc){ | |
this.setScaleSize(); | |
} else { | |
this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2); | |
} | |
this.buildYLabels(); | |
}, | |
buildYLabels: function(){ | |
this.yLabels = []; | |
var stepDecimalPlaces = getDecimalPlaces(this.stepValue); | |
for (var i=0; i<=this.steps; i++){ | |
this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)})); | |
} | |
}, | |
getCircumference : function(){ | |
return ((Math.PI*2) / this.valuesCount); | |
}, | |
setScaleSize: function(){ | |
/* | |
* Right, this is really confusing and there is a lot of maths going on here | |
* The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 | |
* | |
* Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif | |
* | |
* Solution: | |
* | |
* We assume the radius of the polygon is half the size of the canvas at first | |
* at each index we check if the text overlaps. | |
* | |
* Where it does, we store that angle and that index. | |
* | |
* After finding the largest index and angle we calculate how much we need to remove | |
* from the shape radius to move the point inwards by that x. | |
* | |
* We average the left and right distances to get the maximum shape radius that can fit in the box | |
* along with labels. | |
* | |
* Once we have that, we can find the centre point for the chart, by taking the x text protrusion | |
* on each side, removing that from the size, halving it and adding the left x protrusion width. | |
* | |
* This will mean we have a shape fitted to the canvas, as large as it can be with the labels | |
* and position it in the most space efficient manner | |
* | |
* https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif | |
*/ | |
// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. | |
// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points | |
var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]), | |
pointPosition, | |
i, | |
textWidth, | |
halfTextWidth, | |
furthestRight = this.width, | |
furthestRightIndex, | |
furthestRightAngle, | |
furthestLeft = 0, | |
furthestLeftIndex, | |
furthestLeftAngle, | |
xProtrusionLeft, | |
xProtrusionRight, | |
radiusReductionRight, | |
radiusReductionLeft, | |
maxWidthRadius; | |
this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); | |
for (i=0;i<this.valuesCount;i++){ | |
// 5px to space the text slightly out - similar to what we do in the draw function. | |
pointPosition = this.getPointPosition(i, largestPossibleRadius); | |
textWidth = this.ctx.measureText(template(this.templateString, { value: this.labels[i] })).width + 5; | |
if (i === 0 || i === this.valuesCount/2){ | |
// If we're at index zero, or exactly the middle, we're at exactly the top/bottom | |
// of the radar chart, so text will be aligned centrally, so we'll half it and compare | |
// w/left and right text sizes | |
halfTextWidth = textWidth/2; | |
if (pointPosition.x + halfTextWidth > furthestRight) { | |
furthestRight = pointPosition.x + halfTextWidth; | |
furthestRightIndex = i; | |
} | |
if (pointPosition.x - halfTextWidth < furthestLeft) { | |
furthestLeft = pointPosition.x - halfTextWidth; | |
furthestLeftIndex = i; | |
} | |
} | |
else if (i < this.valuesCount/2) { | |
// Less than half the values means we'll left align the text | |
if (pointPosition.x + textWidth > furthestRight) { | |
furthestRight = pointPosition.x + textWidth; | |
furthestRightIndex = i; | |
} | |
} | |
else if (i > this.valuesCount/2){ | |
// More than half the values means we'll right align the text | |
if (pointPosition.x - textWidth < furthestLeft) { | |
furthestLeft = pointPosition.x - textWidth; | |
furthestLeftIndex = i; | |
} | |
} | |
} | |
xProtrusionLeft = furthestLeft; | |
xProtrusionRight = Math.ceil(furthestRight - this.width); | |
furthestRightAngle = this.getIndexAngle(furthestRightIndex); | |
furthestLeftAngle = this.getIndexAngle(furthestLeftIndex); | |
radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2); | |
radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2); | |
// Ensure we actually need to reduce the size of the chart | |
radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0; | |
radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0; | |
this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2; | |
//this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2]) | |
this.setCenterPoint(radiusReductionLeft, radiusReductionRight); | |
}, | |
setCenterPoint: function(leftMovement, rightMovement){ | |
var maxRight = this.width - rightMovement - this.drawingArea, | |
maxLeft = leftMovement + this.drawingArea; | |
this.xCenter = (maxLeft + maxRight)/2; | |
// Always vertically in the centre as the text height doesn't change | |
this.yCenter = (this.height/2); | |
}, | |
getIndexAngle : function(index){ | |
var angleMultiplier = (Math.PI * 2) / this.valuesCount; | |
// Start from the top instead of right, so remove a quarter of the circle | |
return index * angleMultiplier - (Math.PI/2); | |
}, | |
getPointPosition : function(index, distanceFromCenter){ | |
var thisAngle = this.getIndexAngle(index); | |
return { | |
x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter, | |
y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter | |
}; | |
}, | |
draw: function(){ | |
if (this.display){ | |
var ctx = this.ctx; | |
each(this.yLabels, function(label, index){ | |
// Don't draw a centre value | |
if (index > 0){ | |
var yCenterOffset = index * (this.drawingArea/this.steps), | |
yHeight = this.yCenter - yCenterOffset, | |
pointPosition; | |
// Draw circular lines around the scale | |
if (this.lineWidth > 0){ | |
ctx.strokeStyle = this.lineColor; | |
ctx.lineWidth = this.lineWidth; | |
if(this.lineArc){ | |
ctx.beginPath(); | |
ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2); | |
ctx.closePath(); | |
ctx.stroke(); | |
} else{ | |
ctx.beginPath(); | |
for (var i=0;i<this.valuesCount;i++) | |
{ | |
pointPosition = this.getPointPosition(i, this.calculateCenterOffset(this.min + (index * this.stepValue))); | |
if (i === 0){ | |
ctx.moveTo(pointPosition.x, pointPosition.y); | |
} else { | |
ctx.lineTo(pointPosition.x, pointPosition.y); | |
} | |
} | |
ctx.closePath(); | |
ctx.stroke(); | |
} | |
} | |
if(this.showLabels){ | |
ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily); | |
if (this.showLabelBackdrop){ | |
var labelWidth = ctx.measureText(label).width; | |
ctx.fillStyle = this.backdropColor; | |
ctx.fillRect( | |
this.xCenter - labelWidth/2 - this.backdropPaddingX, | |
yHeight - this.fontSize/2 - this.backdropPaddingY, | |
labelWidth + this.backdropPaddingX*2, | |
this.fontSize + this.backdropPaddingY*2 | |
); | |
} | |
ctx.textAlign = 'center'; | |
ctx.textBaseline = "middle"; | |
ctx.fillStyle = this.fontColor; | |
ctx.fillText(label, this.xCenter, yHeight); | |
} | |
} | |
}, this); | |
if (!this.lineArc){ | |
ctx.lineWidth = this.angleLineWidth; | |
ctx.strokeStyle = this.angleLineColor; | |
for (var i = this.valuesCount - 1; i >= 0; i--) { | |
if (this.angleLineWidth > 0){ | |
var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max)); | |
ctx.beginPath(); | |
ctx.moveTo(this.xCenter, this.yCenter); | |
ctx.lineTo(outerPosition.x, outerPosition.y); | |
ctx.stroke(); | |
ctx.closePath(); | |
} | |
// Extra 3px out for some label spacing | |
var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5); | |
ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily); | |
ctx.fillStyle = this.pointLabelFontColor; | |
var labelsCount = this.labels.length, | |
halfLabelsCount = this.labels.length/2, | |
quarterLabelsCount = halfLabelsCount/2, | |
upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount), | |
exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount); | |
if (i === 0){ | |
ctx.textAlign = 'center'; | |
} else if(i === halfLabelsCount){ | |
ctx.textAlign = 'center'; | |
} else if (i < halfLabelsCount){ | |
ctx.textAlign = 'left'; | |
} else { | |
ctx.textAlign = 'right'; | |
} | |
// Set the correct text baseline based on outer positioning | |
if (exactQuarter){ | |
ctx.textBaseline = 'middle'; | |
} else if (upperHalf){ | |
ctx.textBaseline = 'bottom'; | |
} else { | |
ctx.textBaseline = 'top'; | |
} | |
ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y); | |
} | |
} | |
} | |
} | |
}); | |
// Attach global event to resize each chart instance when the browser resizes | |
helpers.addEvent(window, "resize", (function(){ | |
// Basic debounce of resize function so it doesn't hurt performance when resizing browser. | |
var timeout; | |
return function(){ | |
clearTimeout(timeout); | |
timeout = setTimeout(function(){ | |
each(Chart.instances,function(instance){ | |
// If the responsive flag is set in the chart instance config | |
// Cascade the resize event down to the chart. | |
if (instance.options.responsive){ | |
instance.resize(instance.render, true); | |
} | |
}); | |
}, 50); | |
}; | |
})()); | |
if (amd) { | |
define(function(){ | |
return Chart; | |
}); | |
} else if (typeof module === 'object' && module.exports) { | |
module.exports = Chart; | |
} | |
root.Chart = Chart; | |
Chart.noConflict = function(){ | |
root.Chart = previous; | |
return Chart; | |
}; | |
}).call(this); |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
//Cache a local reference to Chart.helpers | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
//Boolean - Whether we should show a stroke on each segment | |
segmentShowStroke : true, | |
//String - The colour of each segment stroke | |
segmentStrokeColor : "#fff", | |
//Number - The width of each segment stroke | |
segmentStrokeWidth : 2, | |
//The percentage of the chart that we cut out of the middle. | |
percentageInnerCutout : 50, | |
//Number - Amount of animation steps | |
animationSteps : 100, | |
//String - Animation easing effect | |
animationEasing : "easeOutBounce", | |
//Boolean - Whether we animate the rotation of the Doughnut | |
animateRotate : true, | |
//Boolean - Whether we animate scaling the Doughnut from the centre | |
animateScale : false, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
//Passing in a name registers this chart in the Chart namespace | |
name: "Doughnut", | |
//Providing a defaults will also register the deafults in the chart namespace | |
defaults : defaultConfig, | |
//Initialize is fired when the chart is initialized - Data is passed in as a parameter | |
//Config is automatically merged by the core of Chart.js, and is available at this.options | |
initialize: function(data){ | |
//Declare segments as a static property to prevent inheriting across the Chart type prototype | |
this.segments = []; | |
this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; | |
this.SegmentArc = Chart.Arc.extend({ | |
ctx : this.chart.ctx, | |
x : this.chart.width/2, | |
y : this.chart.height/2 | |
}); | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; | |
helpers.each(this.segments,function(segment){ | |
segment.restore(["fillColor"]); | |
}); | |
helpers.each(activeSegments,function(activeSegment){ | |
activeSegment.fillColor = activeSegment.highlightColor; | |
}); | |
this.showTooltip(activeSegments); | |
}); | |
} | |
this.calculateTotal(data); | |
helpers.each(data,function(datapoint, index){ | |
this.addData(datapoint, index, true); | |
},this); | |
this.render(); | |
}, | |
getSegmentsAtEvent : function(e){ | |
var segmentsArray = []; | |
var location = helpers.getRelativePosition(e); | |
helpers.each(this.segments,function(segment){ | |
if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); | |
},this); | |
return segmentsArray; | |
}, | |
addData : function(segment, atIndex, silent){ | |
var index = atIndex || this.segments.length; | |
this.segments.splice(index, 0, new this.SegmentArc({ | |
value : segment.value, | |
outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, | |
innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, | |
fillColor : segment.color, | |
highlightColor : segment.highlight || segment.color, | |
showStroke : this.options.segmentShowStroke, | |
strokeWidth : this.options.segmentStrokeWidth, | |
strokeColor : this.options.segmentStrokeColor, | |
startAngle : Math.PI * 1.5, | |
circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), | |
label : segment.label | |
})); | |
if (!silent){ | |
this.reflow(); | |
this.update(); | |
} | |
}, | |
calculateCircumference : function(value){ | |
return (Math.PI*2)*(value / this.total); | |
}, | |
calculateTotal : function(data){ | |
this.total = 0; | |
helpers.each(data,function(segment){ | |
this.total += segment.value; | |
},this); | |
}, | |
update : function(){ | |
this.calculateTotal(this.segments); | |
// Reset any highlight colours before updating. | |
helpers.each(this.activeElements, function(activeElement){ | |
activeElement.restore(['fillColor']); | |
}); | |
helpers.each(this.segments,function(segment){ | |
segment.save(); | |
}); | |
this.render(); | |
}, | |
removeData: function(atIndex){ | |
var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; | |
this.segments.splice(indexToDelete, 1); | |
this.reflow(); | |
this.update(); | |
}, | |
reflow : function(){ | |
helpers.extend(this.SegmentArc.prototype,{ | |
x : this.chart.width/2, | |
y : this.chart.height/2 | |
}); | |
this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; | |
helpers.each(this.segments, function(segment){ | |
segment.update({ | |
outerRadius : this.outerRadius, | |
innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout | |
}); | |
}, this); | |
}, | |
draw : function(easeDecimal){ | |
var animDecimal = (easeDecimal) ? easeDecimal : 1; | |
this.clear(); | |
helpers.each(this.segments,function(segment,index){ | |
segment.transition({ | |
circumference : this.calculateCircumference(segment.value), | |
outerRadius : this.outerRadius, | |
innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout | |
},animDecimal); | |
segment.endAngle = segment.startAngle + segment.circumference; | |
segment.draw(); | |
if (index === 0){ | |
segment.startAngle = Math.PI * 1.5; | |
} | |
//Check to see if it's the last segment, if not get the next and update the start angle | |
if (index < this.segments.length-1){ | |
this.segments[index+1].startAngle = segment.endAngle; | |
} | |
},this); | |
} | |
}); | |
Chart.types.Doughnut.extend({ | |
name : "Pie", | |
defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) | |
}); | |
}).call(this); |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
///Boolean - Whether grid lines are shown across the chart | |
scaleShowGridLines : true, | |
//String - Colour of the grid lines | |
scaleGridLineColor : "rgba(0,0,0,.05)", | |
//Number - Width of the grid lines | |
scaleGridLineWidth : 1, | |
//Boolean - Whether the line is curved between points | |
bezierCurve : true, | |
//Number - Tension of the bezier curve between points | |
bezierCurveTension : 0.4, | |
//Boolean - Whether to show a dot for each point | |
pointDot : true, | |
//Number - Radius of each point dot in pixels | |
pointDotRadius : 4, | |
//Number - Pixel width of point dot stroke | |
pointDotStrokeWidth : 1, | |
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point | |
pointHitDetectionRadius : 20, | |
//Boolean - Whether to show a stroke for datasets | |
datasetStroke : true, | |
//Number - Pixel width of dataset stroke | |
datasetStrokeWidth : 2, | |
//Boolean - Whether to fill the dataset with a colour | |
datasetFill : true, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
name: "Line", | |
defaults : defaultConfig, | |
initialize: function(data){ | |
//Declare the extension of the default point, to cater for the options passed in to the constructor | |
this.PointClass = Chart.Point.extend({ | |
strokeWidth : this.options.pointDotStrokeWidth, | |
radius : this.options.pointDotRadius, | |
display: this.options.pointDot, | |
hitDetectionRadius : this.options.pointHitDetectionRadius, | |
ctx : this.chart.ctx, | |
inRange : function(mouseX){ | |
return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); | |
} | |
}); | |
this.datasets = []; | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; | |
this.eachPoints(function(point){ | |
point.restore(['fillColor', 'strokeColor']); | |
}); | |
helpers.each(activePoints, function(activePoint){ | |
activePoint.fillColor = activePoint.highlightFill; | |
activePoint.strokeColor = activePoint.highlightStroke; | |
}); | |
this.showTooltip(activePoints); | |
}); | |
} | |
//Iterate through each of the datasets, and build this into a property of the chart | |
helpers.each(data.datasets,function(dataset){ | |
var datasetObject = { | |
label : dataset.label || null, | |
fillColor : dataset.fillColor, | |
strokeColor : dataset.strokeColor, | |
pointColor : dataset.pointColor, | |
pointStrokeColor : dataset.pointStrokeColor, | |
points : [] | |
}; | |
this.datasets.push(datasetObject); | |
helpers.each(dataset.data,function(dataPoint,index){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
datasetObject.points.push(new this.PointClass({ | |
value : dataPoint, | |
label : data.labels[index], | |
datasetLabel: dataset.label, | |
strokeColor : dataset.pointStrokeColor, | |
fillColor : dataset.pointColor, | |
highlightFill : dataset.pointHighlightFill || dataset.pointColor, | |
highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor | |
})); | |
},this); | |
this.buildScale(data.labels); | |
this.eachPoints(function(point, index){ | |
helpers.extend(point, { | |
x: this.scale.calculateX(index), | |
y: this.scale.endPoint | |
}); | |
point.save(); | |
}, this); | |
},this); | |
this.render(); | |
}, | |
update : function(){ | |
this.scale.update(); | |
// Reset any highlight colours before updating. | |
helpers.each(this.activeElements, function(activeElement){ | |
activeElement.restore(['fillColor', 'strokeColor']); | |
}); | |
this.eachPoints(function(point){ | |
point.save(); | |
}); | |
this.render(); | |
}, | |
eachPoints : function(callback){ | |
helpers.each(this.datasets,function(dataset){ | |
helpers.each(dataset.points,callback,this); | |
},this); | |
}, | |
getPointsAtEvent : function(e){ | |
var pointsArray = [], | |
eventPosition = helpers.getRelativePosition(e); | |
helpers.each(this.datasets,function(dataset){ | |
helpers.each(dataset.points,function(point){ | |
if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); | |
}); | |
},this); | |
return pointsArray; | |
}, | |
buildScale : function(labels){ | |
var self = this; | |
var dataTotal = function(){ | |
var values = []; | |
self.eachPoints(function(point){ | |
values.push(point.value); | |
}); | |
return values; | |
}; | |
var scaleOptions = { | |
templateString : this.options.scaleLabel, | |
height : this.chart.height, | |
width : this.chart.width, | |
ctx : this.chart.ctx, | |
textColor : this.options.scaleFontColor, | |
fontSize : this.options.scaleFontSize, | |
fontStyle : this.options.scaleFontStyle, | |
fontFamily : this.options.scaleFontFamily, | |
valuesCount : labels.length, | |
beginAtZero : this.options.scaleBeginAtZero, | |
integersOnly : this.options.scaleIntegersOnly, | |
calculateYRange : function(currentHeight){ | |
var updatedRanges = helpers.calculateScaleRange( | |
dataTotal(), | |
currentHeight, | |
this.fontSize, | |
this.beginAtZero, | |
this.integersOnly | |
); | |
helpers.extend(this, updatedRanges); | |
}, | |
xLabels : labels, | |
font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), | |
lineWidth : this.options.scaleLineWidth, | |
lineColor : this.options.scaleLineColor, | |
gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, | |
gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", | |
padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, | |
showLabels : this.options.scaleShowLabels, | |
display : this.options.showScale | |
}; | |
if (this.options.scaleOverride){ | |
helpers.extend(scaleOptions, { | |
calculateYRange: helpers.noop, | |
steps: this.options.scaleSteps, | |
stepValue: this.options.scaleStepWidth, | |
min: this.options.scaleStartValue, | |
max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) | |
}); | |
} | |
this.scale = new Chart.Scale(scaleOptions); | |
}, | |
addData : function(valuesArray,label){ | |
//Map the values array for each of the datasets | |
helpers.each(valuesArray,function(value,datasetIndex){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
this.datasets[datasetIndex].points.push(new this.PointClass({ | |
value : value, | |
label : label, | |
x: this.scale.calculateX(this.scale.valuesCount+1), | |
y: this.scale.endPoint, | |
strokeColor : this.datasets[datasetIndex].pointStrokeColor, | |
fillColor : this.datasets[datasetIndex].pointColor | |
})); | |
},this); | |
this.scale.addXLabel(label); | |
//Then re-render the chart. | |
this.update(); | |
}, | |
removeData : function(){ | |
this.scale.removeXLabel(); | |
//Then re-render the chart. | |
helpers.each(this.datasets,function(dataset){ | |
dataset.points.shift(); | |
},this); | |
this.update(); | |
}, | |
reflow : function(){ | |
var newScaleProps = helpers.extend({ | |
height : this.chart.height, | |
width : this.chart.width | |
}); | |
this.scale.update(newScaleProps); | |
}, | |
draw : function(ease){ | |
var easingDecimal = ease || 1; | |
this.clear(); | |
var ctx = this.chart.ctx; | |
// Some helper methods for getting the next/prev points | |
var hasValue = function(item){ | |
return item.value !== null; | |
}, | |
nextPoint = function(point, collection, index){ | |
return helpers.findNextWhere(collection, hasValue, index) || point; | |
}, | |
previousPoint = function(point, collection, index){ | |
return helpers.findPreviousWhere(collection, hasValue, index) || point; | |
}; | |
this.scale.draw(easingDecimal); | |
helpers.each(this.datasets,function(dataset){ | |
var pointsWithValues = helpers.where(dataset.points, hasValue); | |
//Transition each point first so that the line and point drawing isn't out of sync | |
//We can use this extra loop to calculate the control points of this dataset also in this loop | |
helpers.each(dataset.points, function(point, index){ | |
if (point.hasValue()){ | |
point.transition({ | |
y : this.scale.calculateY(point.value), | |
x : this.scale.calculateX(index) | |
}, easingDecimal); | |
} | |
},this); | |
// Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point | |
// This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed | |
if (this.options.bezierCurve){ | |
helpers.each(pointsWithValues, function(point, index){ | |
var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; | |
point.controlPoints = helpers.splineCurve( | |
previousPoint(point, pointsWithValues, index), | |
point, | |
nextPoint(point, pointsWithValues, index), | |
tension | |
); | |
// Prevent the bezier going outside of the bounds of the graph | |
// Cap puter bezier handles to the upper/lower scale bounds | |
if (point.controlPoints.outer.y > this.scale.endPoint){ | |
point.controlPoints.outer.y = this.scale.endPoint; | |
} | |
else if (point.controlPoints.outer.y < this.scale.startPoint){ | |
point.controlPoints.outer.y = this.scale.startPoint; | |
} | |
// Cap inner bezier handles to the upper/lower scale bounds | |
if (point.controlPoints.inner.y > this.scale.endPoint){ | |
point.controlPoints.inner.y = this.scale.endPoint; | |
} | |
else if (point.controlPoints.inner.y < this.scale.startPoint){ | |
point.controlPoints.inner.y = this.scale.startPoint; | |
} | |
},this); | |
} | |
//Draw the line between all the points | |
ctx.lineWidth = this.options.datasetStrokeWidth; | |
ctx.strokeStyle = dataset.strokeColor; | |
ctx.beginPath(); | |
helpers.each(pointsWithValues, function(point, index){ | |
if (index === 0){ | |
ctx.moveTo(point.x, point.y); | |
} | |
else{ | |
if(this.options.bezierCurve){ | |
var previous = previousPoint(point, pointsWithValues, index); | |
ctx.bezierCurveTo( | |
previous.controlPoints.outer.x, | |
previous.controlPoints.outer.y, | |
point.controlPoints.inner.x, | |
point.controlPoints.inner.y, | |
point.x, | |
point.y | |
); | |
} | |
else{ | |
ctx.lineTo(point.x,point.y); | |
} | |
} | |
}, this); | |
ctx.stroke(); | |
if (this.options.datasetFill && pointsWithValues.length > 0){ | |
//Round off the line by going to the base of the chart, back to the start, then fill. | |
ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); | |
ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); | |
ctx.fillStyle = dataset.fillColor; | |
ctx.closePath(); | |
ctx.fill(); | |
} | |
//Now draw the points over the line | |
//A little inefficient double looping, but better than the line | |
//lagging behind the point positions | |
helpers.each(pointsWithValues,function(point){ | |
point.draw(); | |
}); | |
},this); | |
} | |
}); | |
}).call(this); |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
//Cache a local reference to Chart.helpers | |
helpers = Chart.helpers; | |
var defaultConfig = { | |
//Boolean - Show a backdrop to the scale label | |
scaleShowLabelBackdrop : true, | |
//String - The colour of the label backdrop | |
scaleBackdropColor : "rgba(255,255,255,0.75)", | |
// Boolean - Whether the scale should begin at zero | |
scaleBeginAtZero : true, | |
//Number - The backdrop padding above & below the label in pixels | |
scaleBackdropPaddingY : 2, | |
//Number - The backdrop padding to the side of the label in pixels | |
scaleBackdropPaddingX : 2, | |
//Boolean - Show line for each value in the scale | |
scaleShowLine : true, | |
//Boolean - Stroke a line around each segment in the chart | |
segmentShowStroke : true, | |
//String - The colour of the stroke on each segement. | |
segmentStrokeColor : "#fff", | |
//Number - The width of the stroke value in pixels | |
segmentStrokeWidth : 2, | |
//Number - Amount of animation steps | |
animationSteps : 100, | |
//String - Animation easing effect. | |
animationEasing : "easeOutBounce", | |
//Boolean - Whether to animate the rotation of the chart | |
animateRotate : true, | |
//Boolean - Whether to animate scaling the chart from the centre | |
animateScale : false, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<segments.length; i++){%><li><span style=\"background-color:<%=segments[i].fillColor%>\"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>" | |
}; | |
Chart.Type.extend({ | |
//Passing in a name registers this chart in the Chart namespace | |
name: "PolarArea", | |
//Providing a defaults will also register the deafults in the chart namespace | |
defaults : defaultConfig, | |
//Initialize is fired when the chart is initialized - Data is passed in as a parameter | |
//Config is automatically merged by the core of Chart.js, and is available at this.options | |
initialize: function(data){ | |
this.segments = []; | |
//Declare segment class as a chart instance specific class, so it can share props for this instance | |
this.SegmentArc = Chart.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 Chart.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: true, | |
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: data.length | |
}); | |
this.updateScaleRange(data); | |
this.scale.update(); | |
helpers.each(data,function(segment,index){ | |
this.addData(segment,index,true); | |
},this); | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; | |
helpers.each(this.segments,function(segment){ | |
segment.restore(["fillColor"]); | |
}); | |
helpers.each(activeSegments,function(activeSegment){ | |
activeSegment.fillColor = activeSegment.highlightColor; | |
}); | |
this.showTooltip(activeSegments); | |
}); | |
} | |
this.render(); | |
}, | |
getSegmentsAtEvent : function(e){ | |
var segmentsArray = []; | |
var location = helpers.getRelativePosition(e); | |
helpers.each(this.segments,function(segment){ | |
if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); | |
},this); | |
return segmentsArray; | |
}, | |
addData : function(segment, atIndex, silent){ | |
var index = atIndex || this.segments.length; | |
this.segments.splice(index, 0, new this.SegmentArc({ | |
fillColor: segment.color, | |
highlightColor: segment.highlight || segment.color, | |
label: segment.label, | |
value: segment.value, | |
outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), | |
circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), | |
startAngle: Math.PI * 1.5 | |
})); | |
if (!silent){ | |
this.reflow(); | |
this.update(); | |
} | |
}, | |
removeData: function(atIndex){ | |
var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; | |
this.segments.splice(indexToDelete, 1); | |
this.reflow(); | |
this.update(); | |
}, | |
calculateTotal: function(data){ | |
this.total = 0; | |
helpers.each(data,function(segment){ | |
this.total += segment.value; | |
},this); | |
this.scale.valuesCount = this.segments.length; | |
}, | |
updateScaleRange: function(datapoints){ | |
var valuesArray = []; | |
helpers.each(datapoints,function(segment){ | |
valuesArray.push(segment.value); | |
}); | |
var scaleSizes = (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) | |
} : | |
helpers.calculateScaleRange( | |
valuesArray, | |
helpers.min([this.chart.width, this.chart.height])/2, | |
this.options.scaleFontSize, | |
this.options.scaleBeginAtZero, | |
this.options.scaleIntegersOnly | |
); | |
helpers.extend( | |
this.scale, | |
scaleSizes, | |
{ | |
size: helpers.min([this.chart.width, this.chart.height]), | |
xCenter: this.chart.width/2, | |
yCenter: this.chart.height/2 | |
} | |
); | |
}, | |
update : function(){ | |
this.calculateTotal(this.segments); | |
helpers.each(this.segments,function(segment){ | |
segment.save(); | |
}); | |
this.render(); | |
}, | |
reflow : function(){ | |
helpers.extend(this.SegmentArc.prototype,{ | |
x : this.chart.width/2, | |
y : this.chart.height/2 | |
}); | |
this.updateScaleRange(this.segments); | |
this.scale.update(); | |
helpers.extend(this.scale,{ | |
xCenter: this.chart.width/2, | |
yCenter: this.chart.height/2 | |
}); | |
helpers.each(this.segments, function(segment){ | |
segment.update({ | |
outerRadius : this.scale.calculateCenterOffset(segment.value) | |
}); | |
}, this); | |
}, | |
draw : function(ease){ | |
var easingDecimal = ease || 1; | |
//Clear & draw the canvas | |
this.clear(); | |
helpers.each(this.segments,function(segment, index){ | |
segment.transition({ | |
circumference : this.scale.getCircumference(), | |
outerRadius : this.scale.calculateCenterOffset(segment.value) | |
},easingDecimal); | |
segment.endAngle = segment.startAngle + segment.circumference; | |
// If we've removed the first segment we need to set the first one to | |
// start at the top. | |
if (index === 0){ | |
segment.startAngle = Math.PI * 1.5; | |
} | |
//Check to see if it's the last segment, if not get the next and update the start angle | |
if (index < this.segments.length - 1){ | |
this.segments[index+1].startAngle = segment.endAngle; | |
} | |
segment.draw(); | |
}, this); | |
this.scale.draw(); | |
} | |
}); | |
}).call(this); |
(function(){ | |
"use strict"; | |
var root = this, | |
Chart = root.Chart, | |
helpers = Chart.helpers; | |
Chart.Type.extend({ | |
name: "Radar", | |
defaults:{ | |
//Boolean - Whether to show lines for each scale point | |
scaleShowLine : true, | |
//Boolean - Whether we show the angle lines out of the radar | |
angleShowLineOut : true, | |
//Boolean - Whether to show labels on the scale | |
scaleShowLabels : false, | |
// Boolean - Whether the scale should begin at zero | |
scaleBeginAtZero : true, | |
//String - Colour of the angle line | |
angleLineColor : "rgba(0,0,0,.1)", | |
//Number - Pixel width of the angle line | |
angleLineWidth : 1, | |
//String - Point label font declaration | |
pointLabelFontFamily : "'Arial'", | |
//String - Point label font weight | |
pointLabelFontStyle : "normal", | |
//Number - Point label font size in pixels | |
pointLabelFontSize : 10, | |
//String - Point label font colour | |
pointLabelFontColor : "#666", | |
//Boolean - Whether to show a dot for each point | |
pointDot : true, | |
//Number - Radius of each point dot in pixels | |
pointDotRadius : 3, | |
//Number - Pixel width of point dot stroke | |
pointDotStrokeWidth : 1, | |
//Number - amount extra to add to the radius to cater for hit detection outside the drawn point | |
pointHitDetectionRadius : 20, | |
//Boolean - Whether to show a stroke for datasets | |
datasetStroke : true, | |
//Number - Pixel width of dataset stroke | |
datasetStrokeWidth : 2, | |
//Boolean - Whether to fill the dataset with a colour | |
datasetFill : true, | |
//String - A legend template | |
legendTemplate : "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>" | |
}, | |
initialize: function(data){ | |
this.PointClass = Chart.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(data); | |
//Set up tooltip events on the chart | |
if (this.options.showTooltips){ | |
helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ | |
var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; | |
this.eachPoints(function(point){ | |
point.restore(['fillColor', 'strokeColor']); | |
}); | |
helpers.each(activePointsCollection, function(activePoint){ | |
activePoint.fillColor = activePoint.highlightFill; | |
activePoint.strokeColor = activePoint.highlightStroke; | |
}); | |
this.showTooltip(activePointsCollection); | |
}); | |
} | |
//Iterate through each of the datasets, and build this into a property of the chart | |
helpers.each(data.datasets,function(dataset){ | |
var datasetObject = { | |
label: dataset.label || null, | |
fillColor : dataset.fillColor, | |
strokeColor : dataset.strokeColor, | |
pointColor : dataset.pointColor, | |
pointStrokeColor : dataset.pointStrokeColor, | |
points : [] | |
}; | |
this.datasets.push(datasetObject); | |
helpers.each(dataset.data,function(dataPoint,index){ | |
//Add a new point for each piece of data, passing any required data to draw. | |
var pointPosition; | |
if (!this.scale.animation){ | |
pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); | |
} | |
datasetObject.points.push(new this.PointClass({ | |
value : dataPoint, | |
label : data.labels[index], | |
datasetLabel: dataset.label, | |
x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, | |
y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, | |
strokeColor : dataset.pointStrokeColor, | |
fillColor : dataset.pointColor, | |
highlightFill : dataset.pointHighlightFill || dataset.pointColor, | |
highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor | |
})); | |
},this); | |
},this); | |
this.render(); | |
}, | |
eachPoints : function(callback){ | |
helpers.each(this.datasets,function(dataset){ | |
helpers.each(dataset.points,callback,this); | |
},this); | |
}, | |
getPointsAtEvent : function(evt){ | |
var mousePosition = helpers.getRelativePosition(evt), | |
fromCenter = helpers.getAngleFromPoint({ | |
x: this.scale.xCenter, | |
y: this.scale.yCenter | |
}, mousePosition); | |
var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, | |
pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), | |
activePointsCollection = []; | |
// If we're at the top, make the pointIndex 0 to get the first of the array. | |
if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ | |
pointIndex = 0; | |
} | |
if (fromCenter.distance <= this.scale.drawingArea){ | |
helpers.each(this.datasets, function(dataset){ | |
activePointsCollection.push(dataset.points[pointIndex]); | |
}); | |
} | |
return activePointsCollection; | |
}, | |
buildScale : function(data){ | |
this.scale = new Chart.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, | |
// Point labels at the edge of each line | |
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: data.labels, | |
valuesCount: data.datasets[0].data.length | |
}); | |
this.scale.setScaleSize(); | |
this.updateScaleRange(data.datasets); | |
this.scale.buildYLabels(); | |
}, | |
updateScaleRange: function(datasets){ | |
var valuesArray = (function(){ | |
var totalDataArray = []; | |
helpers.each(datasets,function(dataset){ | |
if (dataset.data){ | |
totalDataArray = totalDataArray.concat(dataset.data); | |
} | |
else { | |
helpers.each(dataset.points, function(point){ | |
totalDataArray.push(point.value); | |
}); | |
} | |
}); | |
return totalDataArray; | |
})(); | |
var scaleSizes = (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) | |
} : | |
helpers.calculateScaleRange( | |
valuesArray, | |
helpers.min([this.chart.width, this.chart.height])/2, | |
this.options.scaleFontSize, | |
this.options.scaleBeginAtZero, | |
this.options.scaleIntegersOnly | |
); | |
helpers.extend( | |
this.scale, | |
scaleSizes | |
); | |
}, | |
addData : function(valuesArray,label){ | |
//Map the values array for each of the datasets | |
this.scale.valuesCount++; | |
helpers.each(valuesArray,function(value,datasetIndex){ | |
var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); | |
this.datasets[datasetIndex].points.push(new this.PointClass({ | |
value : value, | |
label : label, | |
x: pointPosition.x, | |
y: pointPosition.y, | |
strokeColor : this.datasets[datasetIndex].pointStrokeColor, | |
fillColor : this.datasets[datasetIndex].pointColor | |
})); | |
},this); | |
this.scale.labels.push(label); | |
this.reflow(); | |
this.update(); | |
}, | |
removeData : function(){ | |
this.scale.valuesCount--; | |
this.scale.labels.shift(); | |
helpers.each(this.datasets,function(dataset){ | |
dataset.points.shift(); | |
},this); | |
this.reflow(); | |
this.update(); | |
}, | |
update : function(){ | |
this.eachPoints(function(point){ | |
point.save(); | |
}); | |
this.reflow(); | |
this.render(); | |
}, | |
reflow: function(){ | |
helpers.extend(this.scale, { | |
width : this.chart.width, | |
height: this.chart.height, | |
size : helpers.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(ease){ | |
var easeDecimal = ease || 1, | |
ctx = this.chart.ctx; | |
this.clear(); | |
this.scale.draw(); | |
helpers.each(this.datasets,function(dataset){ | |
//Transition each point first so that the line and point drawing isn't out of sync | |
helpers.each(dataset.points,function(point,index){ | |
if (point.hasValue()){ | |
point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); | |
} | |
},this); | |
//Draw the line between all the points | |
ctx.lineWidth = this.options.datasetStrokeWidth; | |
ctx.strokeStyle = dataset.strokeColor; | |
ctx.beginPath(); | |
helpers.each(dataset.points,function(point,index){ | |
if (index === 0){ | |
ctx.moveTo(point.x,point.y); | |
} | |
else{ | |
ctx.lineTo(point.x,point.y); | |
} | |
},this); | |
ctx.closePath(); | |
ctx.stroke(); | |
ctx.fillStyle = dataset.fillColor; | |
ctx.fill(); | |
//Now draw the points over the line | |
//A little inefficient double looping, but better than the line | |
//lagging behind the point positions | |
helpers.each(dataset.points,function(point){ | |
if (point.hasValue()){ | |
point.draw(); | |
} | |
}); | |
},this); | |
} | |
}); | |
}).call(this); |
{ | |
"name": "context-free-parser", | |
"private": true, | |
"dependencies": {}, | |
"homepage": "https://github.com/Polymer/context-free-parser", | |
"version": "0.4.2", | |
"_release": "0.4.2", | |
"_resolution": { | |
"type": "version", | |
"tag": "0.4.2", | |
"commit": "6b4ff5a210dc8bd3d24e73f87a6513e32f1e41b6" | |
}, | |
"_source": "git://github.com/Polymer/context-free-parser.git", | |
"_target": "^0.4.0", | |
"_originalSource": "Polymer/context-free-parser" | |
} |
{ | |
"name": "context-free-parser", | |
"private": true, | |
"dependencies": {} | |
} |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<script src="context-free-parser.js"></script> | |
<link rel="import" href="../core-ajax/core-ajax.html"> | |
<!-- | |
Scrapes source documentation data from input text or url. | |
@class context-free-parser | |
--> | |
<polymer-element name="context-free-parser" attributes="url text data"> | |
<template> | |
<core-ajax url="{{url}}" response="{{text}}" auto></core-ajax> | |
</template> | |
<script> | |
Polymer('context-free-parser', { | |
text: null, | |
textChanged: function() { | |
if (this.text) { | |
var entities = ContextFreeParser.parse(this.text); | |
if (!entities || entities.length === 0) { | |
entities = [ | |
{name: this.url.split('/').pop(), description: '**Undocumented**'} | |
]; | |
} | |
this.data = { classes: entities }; | |
} | |
}, | |
dataChanged: function() { | |
this.fire('data-ready'); | |
} | |
}); | |
</script> | |
</polymer-element> |
/* | |
* @license | |
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
* Code distributed by Google as part of the polymer project is also | |
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
*/ | |
(function(scope) { | |
var ContextFreeParser = { | |
parse: function(text) { | |
var top = {}; | |
var entities = []; | |
var current = top; | |
var subCurrent = {}; | |
var scriptDocCommentClause = '\\/\\*\\*([\\s\\S]*?)\\*\\/'; | |
var htmlDocCommentClause = '<!--([\\s\\S]*?)-->'; | |
// matches text between /** and */ inclusive and <!-- and --> inclusive | |
var docCommentRegex = new RegExp(scriptDocCommentClause + '|' + htmlDocCommentClause, 'g'); | |
// acquire all script doc comments | |
var docComments = text.match(docCommentRegex) || []; | |
// each match represents a single block of doc comments | |
docComments.forEach(function(m) { | |
// unify line ends, remove all comment characters, split into individual lines | |
var lines = m.replace(/\r\n/g, '\n').replace(/^\s*\/\*\*|^\s*\*\/|^\s*\* ?|^\s*\<\!-\-|^s*\-\-\>/gm, '').split('\n'); | |
// pragmas (@-rules) must occur on a line by themselves | |
var pragmas = []; | |
// filter lines whose first non-whitespace character is @ into the pragma list | |
// (and out of the `lines` array) | |
lines = lines.filter(function(l) { | |
var m = l.match(/\s*@([\w-]*) (.*)/); | |
if (!m) { | |
return true; | |
} | |
pragmas.push(m); | |
}); | |
// collect all other text into a single block | |
var code = lines.join('\n'); | |
// process pragmas | |
pragmas.forEach(function(m) { | |
var pragma = m[1], content = m[2]; | |
switch (pragma) { | |
// currently all entities are either @class or @element | |
case 'class': | |
case 'element': | |
current = { | |
name: content, | |
description: code | |
}; | |
entities.push(current); | |
break; | |
// an entity may have these describable sub-features | |
case 'attribute': | |
case 'property': | |
case 'method': | |
case 'event': | |
subCurrent = { | |
name: content, | |
description: code | |
}; | |
var label = pragma == 'property' ? 'properties' : pragma + 's'; | |
makePragma(current, label, subCurrent); | |
break; | |
// sub-feature pragmas | |
case 'default': | |
case 'type': | |
subCurrent[pragma] = content; | |
break; | |
case 'param': | |
var eventParmsRe = /\{(.+)\}\s+(\w+[.\w+]+)\s+(.*)$/; | |
var params = content.match(eventParmsRe); | |
if (params) { | |
var subEventObj = { | |
type: params[1], | |
name: params[2], | |
description: params[3] | |
}; | |
makePragma(subCurrent, pragma + 's', subEventObj); | |
} | |
break; | |
// everything else | |
default: | |
current[pragma] = content; | |
break; | |
} | |
}); | |
// utility function, yay hoisting | |
function makePragma(object, pragma, content) { | |
var p$ = object; | |
var p = p$[pragma]; | |
if (!p) { | |
p$[pragma] = p = []; | |
} | |
p.push(content); | |
} | |
}); | |
if (entities.length === 0) { | |
entities.push({name: 'Entity', description: '**Undocumented**'}); | |
} | |
return entities; | |
} | |
}; | |
if (typeof module !== 'undefined' && module.exports) { | |
module.exports = ContextFreeParser; | |
} else { | |
scope.ContextFreeParser = ContextFreeParser; | |
} | |
})(this); |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!doctype html> | |
<html> | |
<head> | |
<title>context-free-parser</title> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="context-free-parser.html"> | |
</head> | |
<body unresolved> | |
<context-free-parser url="../core-ajax/core-ajax.html"></context-free-parser> | |
<script> | |
addEventListener('data-ready', function(event) { | |
console.dir(event.target.data); | |
}); | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS | |
--> | |
<html> | |
<head> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="../core-component-page/core-component-page.html"> | |
</head> | |
<body unresolved> | |
<core-component-page></core-component-page> | |
</body> | |
</html> |
See the component landing page for more information.
{ | |
"name": "core-a11y-keys", | |
"private": true, | |
"dependencies": { | |
"polymer": "Polymer/polymer#^0.4.0" | |
}, | |
"ignore": [ | |
"**/.*", | |
"node_modules", | |
"bower_components", | |
"test", | |
"tests" | |
], | |
"homepage": "https://github.com/Polymer/core-a11y-keys", | |
"version": "0.4.2", | |
"_release": "0.4.2", | |
"_resolution": { | |
"type": "version", | |
"tag": "0.4.2", | |
"commit": "54e2dd7067df4bc2dea3546c4507fbb148b5ee0e" | |
}, | |
"_source": "git://github.com/Polymer/core-a11y-keys.git", | |
"_target": "^0.4.0", | |
"_originalSource": "Polymer/core-a11y-keys" | |
} |
{ | |
"name": "core-a11y-keys", | |
"private": true, | |
"dependencies": { | |
"polymer": "Polymer/polymer#^0.4.0" | |
}, | |
"ignore": [ | |
"**/.*", | |
"node_modules", | |
"bower_components", | |
"test", | |
"tests" | |
] | |
} |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!-- | |
`core-a11y-keys` provides a normalized interface for processing keyboard commands that pertain to [WAI-ARIA best | |
practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding). The element takes care of browser differences | |
with respect to Keyboard events and uses an expressive syntax to filter key presses. | |
Use the `keys` attribute to express what combination of keys will trigger the event to fire. | |
Use the `target` attribute to set up event handlers on a specific node. | |
The `keys-pressed` event will fire when one of the key combinations set with the `keys` attribute is pressed. | |
Example: | |
This element will call `arrowHandler` on all arrow keys: | |
<core-a11y-keys target="{{}}" keys="up down left right" on-keys-pressed="{{arrowHandler}}"></core-a11y-keys> | |
Keys Syntax: | |
The `keys` attribute can accepts a space seprated, `+` concatenated set of modifier keys and some common keyboard keys. | |
The common keys are `a-z`, `0-9` (top row and number pad), `*` (shift 8 and number pad), `F1-F12`, `Page Up`, `Page | |
Down`, `Left Arrow`, `Right Arrow`, `Down Arrow`, `Up Arrow`, `Home`, `End`, `Escape`, `Space`, `Tab`, and `Enter` keys. | |
The modifier keys are `Shift`, `Control`, and `Alt`. | |
All keys are expected to be lowercase and shortened: | |
`Left Arrow` is `left`, `Page Down` is `pagedown`, `Control` is `ctrl`, `F1` is `f1`, `Escape` is `esc` etc. | |
Keys Syntax Example: | |
Given the `keys` attribute value "ctrl+shift+f7 up pagedown esc space alt+m", the `<core-a11y-keys>` element will send | |
the `keys-pressed` event if any of the follow key combos are pressed: Control and Shift and F7 keys, Up Arrow key, Page | |
Down key, Escape key, Space key, Alt and M key. | |
Slider Example: | |
The following is an example of the set of keys that fulfil the WAI-ARIA "slider" role [best | |
practices](http://www.w3.org/TR/wai-aria-practices/#slider): | |
<core-a11y-keys target="{{}}" keys="left pagedown down" on-keys-pressed="{{decrement}}"></core-a11y-keys> | |
<core-a11y-keys target="{{}}" keys="right pageup up" on-keys-pressed="{{increment}}"></core-a11y-keys> | |
<core-a11y-keys target="{{}}" keys="home" on-keys-pressed="{{setMin}}"></core-a11y-keys> | |
<core-a11y-keys target="{{}}" keys="end" on-keys-pressed="{{setMax}}"></core-a11y-keys> | |
The `increment` function will move the slider a set amount toward the maximum value. | |
The `decrement` function will move the slider a set amount toward the minimum value. | |
The `setMin` function will move the slider to the minimum value. | |
The `setMax` function will move the slider to the maximum value. | |
Keys Syntax Grammar: | |
[EBNF](http://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_Form) Grammar of the `keys` attribute. | |
modifier = "shift" | "ctrl" | "alt"; | |
ascii = ? /[a-z0-9]/ ? ; | |
fnkey = ? f1 through f12 ? ; | |
arrow = "up" | "down" | "left" | "right" ; | |
key = "tab" | "esc" | "space" | "*" | "pageup" | "pagedown" | "home" | "end" | arrow | ascii | fnkey ; | |
keycombo = { modifier, "+" }, key ; | |
keys = keycombo, { " ", keycombo } ; | |
@group Core Elements | |
@element core-a11y-keys | |
@homepage github.io | |
--> | |
<link rel="import" href="../polymer/polymer.html"> | |
<style shim-shadowdom> | |
html /deep/ core-a11y-keys { | |
display: none; | |
} | |
</style> | |
<polymer-element name="core-a11y-keys"> | |
<script> | |
(function() { | |
/* | |
* Chrome uses an older version of DOM Level 3 Keyboard Events | |
* | |
* Most keys are labeled as text, but some are Unicode codepoints. | |
* Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set | |
*/ | |
var KEY_IDENTIFIER = { | |
'U+0009': 'tab', | |
'U+001B': 'esc', | |
'U+0020': 'space', | |
'U+002A': '*', | |
'U+0030': '0', | |
'U+0031': '1', | |
'U+0032': '2', | |
'U+0033': '3', | |
'U+0034': '4', | |
'U+0035': '5', | |
'U+0036': '6', | |
'U+0037': '7', | |
'U+0038': '8', | |
'U+0039': '9', | |
'U+0041': 'a', | |
'U+0042': 'b', | |
'U+0043': 'c', | |
'U+0044': 'd', | |
'U+0045': 'e', | |
'U+0046': 'f', | |
'U+0047': 'g', | |
'U+0048': 'h', | |
'U+0049': 'i', | |
'U+004A': 'j', | |
'U+004B': 'k', | |
'U+004C': 'l', | |
'U+004D': 'm', | |
'U+004E': 'n', | |
'U+004F': 'o', | |
'U+0050': 'p', | |
'U+0051': 'q', | |
'U+0052': 'r', | |
'U+0053': 's', | |
'U+0054': 't', | |
'U+0055': 'u', | |
'U+0056': 'v', | |
'U+0057': 'w', | |
'U+0058': 'x', | |
'U+0059': 'y', | |
'U+005A': 'z', | |
'U+007F': 'del' | |
}; | |
/* | |
* Special table for KeyboardEvent.keyCode. | |
* KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better than that | |
* | |
* Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode | |
*/ | |
var KEY_CODE = { | |
13: 'enter', | |
27: 'esc', | |
33: 'pageup', | |
34: 'pagedown', | |
35: 'end', | |
36: 'home', | |
32: 'space', | |
37: 'left', | |
38: 'up', | |
39: 'right', | |
40: 'down', | |
46: 'del', | |
106: '*' | |
}; | |
/* | |
* KeyboardEvent.key is mostly represented by printable character made by the keyboard, with unprintable keys labeled | |
* nicely. | |
* | |
* However, on OS X, Alt+char can make a Unicode character that follows an Apple-specific mapping. In this case, we | |
* fall back to .keyCode. | |
*/ | |
var KEY_CHAR = /[a-z0-9*]/; | |
function transformKey(key) { | |
var validKey = ''; | |
if (key) { | |
var lKey = key.toLowerCase(); | |
if (lKey.length == 1) { | |
if (KEY_CHAR.test(lKey)) { | |
validKey = lKey; | |
} | |
} else if (lKey == 'multiply') { | |
// numpad '*' can map to Multiply on IE/Windows | |
validKey = '*'; | |
} else { | |
validKey = lKey; | |
} | |
} | |
return validKey; | |
} | |
var IDENT_CHAR = /U\+/; | |
function transformKeyIdentifier(keyIdent) { | |
var validKey = ''; | |
if (keyIdent) { | |
if (IDENT_CHAR.test(keyIdent)) { | |
validKey = KEY_IDENTIFIER[keyIdent]; | |
} else { | |
validKey = keyIdent.toLowerCase(); | |
} | |
} | |
return validKey; | |
} | |
function transformKeyCode(keyCode) { | |
var validKey = ''; | |
if (Number(keyCode)) { | |
if (keyCode >= 65 && keyCode <= 90) { | |
// ascii a-z | |
// lowercase is 32 offset from uppercase | |
validKey = String.fromCharCode(32 + keyCode); | |
} else if (keyCode >= 112 && keyCode <= 123) { | |
// function keys f1-f12 | |
validKey = 'f' + (keyCode - 112); | |
} else if (keyCode >= 48 && keyCode <= 57) { | |
// top 0-9 keys | |
validKey = String(48 - keyCode); | |
} else if (keyCode >= 96 && keyCode <= 105) { | |
// num pad 0-9 | |
validKey = String(96 - keyCode); | |
} else { | |
validKey = KEY_CODE[keyCode]; | |
} | |
} | |
return validKey; | |
} | |
function keyboardEventToKey(ev) { | |
// fall back from .key, to .keyIdentifier, and then to .keyCode | |
var normalizedKey = transformKey(ev.key) || transformKeyIdentifier(ev.keyIdentifier) || transformKeyCode(ev.keyCode) || ''; | |
return { | |
shift: ev.shiftKey, | |
ctrl: ev.ctrlKey, | |
meta: ev.metaKey, | |
alt: ev.altKey, | |
key: normalizedKey | |
}; | |
} | |
/* | |
* Input: ctrl+shift+f7 => {ctrl: true, shift: true, key: 'f7'} | |
* ctrl/space => {ctrl: true} || {key: space} | |
*/ | |
function stringToKey(keyCombo) { | |
var keys = keyCombo.split('+'); | |
var keyObj = Object.create(null); | |
keys.forEach(function(key) { | |
if (key == 'shift') { | |
keyObj.shift = true; | |
} else if (key == 'ctrl') { | |
keyObj.ctrl = true; | |
} else if (key == 'alt') { | |
keyObj.alt = true; | |
} else { | |
keyObj.key = key; | |
} | |
}); | |
return keyObj; | |
} | |
function keyMatches(a, b) { | |
return Boolean(a.alt) == Boolean(b.alt) && Boolean(a.ctrl) == Boolean(b.ctrl) && Boolean(a.shift) == Boolean(b.shift) && a.key === b.key; | |
} | |
/** | |
* Fired when a keycombo in `keys` is pressed. | |
* | |
* @event keys-pressed | |
*/ | |
function processKeys(ev) { | |
var current = keyboardEventToKey(ev); | |
for (var i = 0, dk; i < this._desiredKeys.length; i++) { | |
dk = this._desiredKeys[i]; | |
if (keyMatches(dk, current)) { | |
ev.preventDefault(); | |
ev.stopPropagation(); | |
this.fire('keys-pressed', current, this, false); | |
break; | |
} | |
} | |
} | |
function listen(node, handler) { | |
if (node && node.addEventListener) { | |
node.addEventListener('keydown', handler); | |
} | |
} | |
function unlisten(node, handler) { | |
if (node && node.removeEventListener) { | |
node.removeEventListener('keydown', handler); | |
} | |
} | |
Polymer('core-a11y-keys', { | |
created: function() { | |
this._keyHandler = processKeys.bind(this); | |
}, | |
attached: function() { | |
listen(this.target, this._keyHandler); | |
}, | |
detached: function() { | |
unlisten(this.target, this._keyHandler); | |
}, | |
publish: { | |
/** | |
* The set of key combinations to listen for. | |
* | |
* @attribute keys | |
* @type string (keys syntax) | |
* @default '' | |
*/ | |
keys: '', | |
/** | |
* The node that will fire keyboard events. | |
* | |
* @attribute target | |
* @type Node | |
* @default null | |
*/ | |
target: null | |
}, | |
keysChanged: function() { | |
// * can have multiple mappings: shift+8, * on numpad or Multiply on numpad | |
var normalized = this.keys.replace('*', '* shift+*'); | |
this._desiredKeys = normalized.toLowerCase().split(' ').map(stringToKey); | |
}, | |
targetChanged: function(oldTarget) { | |
unlisten(oldTarget, this._keyHandler); | |
listen(this.target, this._keyHandler); | |
} | |
}); | |
})(); | |
</script> | |
</polymer-element> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Core A11y Keys demo</title> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="core-a11y-keys.html"> | |
</head> | |
<body> | |
<template is="auto-binding"> | |
<style> | |
div { | |
height: 100px; | |
width: 100px; | |
background: gray; | |
} | |
</style> | |
<core-a11y-keys id="a11y" keys="* pageup pagedown left right down up shift+a alt+a home end" target="{{$.target}}" on-keys-pressed="{{print}}"></core-a11y-keys> | |
<div id="target" tabindex="0">focus this</div> | |
<pre id="output"></pre> | |
</template> | |
<script> | |
addEventListener('template-bound', function(ev) { | |
ev.target.print = function(ev) { | |
console.log(ev.detail); | |
this.$.output.textContent += ev.detail.key + ' pressed!\n'; | |
} | |
}); | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS | |
--> | |
<html> | |
<head> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="../core-component-page/core-component-page.html"> | |
</head> | |
<body unresolved> | |
<core-component-page></core-component-page> | |
</body> | |
</html> |
{ | |
"name": "core-ajax", | |
"private": true, | |
"dependencies": { | |
"polymer": "Polymer/polymer#^0.4.0" | |
}, | |
"devDependencies": { | |
"polymer-test-tools": "Polymer/polymer-test-tools#master" | |
}, | |
"homepage": "https://github.com/Polymer/core-ajax", | |
"version": "0.4.2", | |
"_release": "0.4.2", | |
"_resolution": { | |
"type": "version", | |
"tag": "0.4.2", | |
"commit": "e3e62c060b105bedd975f440ddec1f933bd8bec5" | |
}, | |
"_source": "git://github.com/Polymer/core-ajax.git", | |
"_target": "^0.4.0", | |
"_originalSource": "Polymer/core-ajax" | |
} |
{ | |
"name": "core-ajax", | |
"private": true, | |
"dependencies": { | |
"polymer": "Polymer/polymer#^0.4.0" | |
}, | |
"devDependencies": { | |
"polymer-test-tools": "Polymer/polymer-test-tools#master" | |
} | |
} |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!-- | |
@group Polymer Core Elements | |
The `core-ajax` element exposes `XMLHttpRequest` functionality. | |
<core-ajax | |
auto | |
url="http://gdata.youtube.com/feeds/api/videos/" | |
params='{"alt":"json", "q":"chrome"}' | |
handleAs="json" | |
on-core-response="{{handleResponse}}"></core-ajax> | |
With `auto` set to `true`, the element performs a request whenever | |
its `url` or `params` properties are changed. | |
Note: The `params` attribute must be double quoted JSON. | |
You can trigger a request explicitly by calling `go` on the | |
element. | |
@element core-ajax | |
@status beta | |
@homepage github.io | |
--> | |
<link rel="import" href="core-xhr.html"> | |
<polymer-element name="core-ajax" hidden attributes="url handleAs auto params response error method headers body contentType withCredentials"> | |
<script> | |
Polymer('core-ajax', { | |
/** | |
* Fired when a response is received. | |
* | |
* @event core-response | |
*/ | |
/** | |
* Fired when an error is received. | |
* | |
* @event core-error | |
*/ | |
/** | |
* Fired whenever a response or an error is received. | |
* | |
* @event core-complete | |
*/ | |
/** | |
* The URL target of the request. | |
* | |
* @attribute url | |
* @type string | |
* @default '' | |
*/ | |
url: '', | |
/** | |
* Specifies what data to store in the `response` property, and | |
* to deliver as `event.response` in `response` events. | |
* | |
* One of: | |
* | |
* `text`: uses `XHR.responseText`. | |
* | |
* `xml`: uses `XHR.responseXML`. | |
* | |
* `json`: uses `XHR.responseText` parsed as JSON. | |
* | |
* `arraybuffer`: uses `XHR.response`. | |
* | |
* `blob`: uses `XHR.response`. | |
* | |
* `document`: uses `XHR.response`. | |
* | |
* @attribute handleAs | |
* @type string | |
* @default 'text' | |
*/ | |
handleAs: '', | |
/** | |
* If true, automatically performs an Ajax request when either `url` or `params` changes. | |
* | |
* @attribute auto | |
* @type boolean | |
* @default false | |
*/ | |
auto: false, | |
/** | |
* Parameters to send to the specified URL, as JSON. | |
* | |
* @attribute params | |
* @type string (JSON) | |
* @default '' | |
*/ | |
params: '', | |
/** | |
* The response for the most recently made request, or null if it hasn't | |
* completed yet or the request resulted in error. | |
* | |
* @attribute response | |
* @type Object | |
* @default null | |
*/ | |
response: null, | |
/** | |
* The error for the most recently made request, or null if it hasn't | |
* completed yet or the request resulted in success. | |
* | |
* @attribute error | |
* @type Object | |
* @default null | |
*/ | |
error: null, | |
/** | |
* The HTTP method to use such as 'GET', 'POST', 'PUT', or 'DELETE'. | |
* Default is 'GET'. | |
* | |
* @attribute method | |
* @type string | |
* @default '' | |
*/ | |
method: '', | |
/** | |
* HTTP request headers to send. | |
* | |
* Example: | |
* | |
* <core-ajax | |
* auto | |
* url="http://somesite.com" | |
* headers='{"X-Requested-With": "XMLHttpRequest"}' | |
* handleAs="json" | |
* on-core-response="{{handleResponse}}"></core-ajax> | |
* | |
* @attribute headers | |
* @type Object | |
* @default null | |
*/ | |
headers: null, | |
/** | |
* Optional raw body content to send when method === "POST". | |
* | |
* Example: | |
* | |
* <core-ajax method="POST" auto url="http://somesite.com" | |
* body='{"foo":1, "bar":2}'> | |
* </core-ajax> | |
* | |
* @attribute body | |
* @type Object | |
* @default null | |
*/ | |
body: null, | |
/** | |
* Content type to use when sending data. | |
* | |
* @attribute contentType | |
* @type string | |
* @default 'application/x-www-form-urlencoded' | |
*/ | |
contentType: 'application/x-www-form-urlencoded', | |
/** | |
* Set the withCredentials flag on the request. | |
* | |
* @attribute withCredentials | |
* @type boolean | |
* @default false | |
*/ | |
withCredentials: false, | |
/** | |
* Additional properties to send to core-xhr. | |
* | |
* Can be set to an object containing default properties | |
* to send as arguments to the `core-xhr.request()` method | |
* which implements the low-level communication. | |
* | |
* @property xhrArgs | |
* @type Object | |
* @default null | |
*/ | |
xhrArgs: null, | |
ready: function() { | |
this.xhr = document.createElement('core-xhr'); | |
}, | |
receive: function(response, xhr) { | |
if (this.isSuccess(xhr)) { | |
this.processResponse(xhr); | |
} else { | |
this.processError(xhr); | |
} | |
this.complete(xhr); | |
}, | |
isSuccess: function(xhr) { | |
var status = xhr.status || 0; | |
return !status || (status >= 200 && status < 300); | |
}, | |
processResponse: function(xhr) { | |
var response = this.evalResponse(xhr); | |
if (xhr === this.activeRequest) { | |
this.response = response; | |
} | |
this.fire('core-response', {response: response, xhr: xhr}); | |
}, | |
processError: function(xhr) { | |
var response = xhr.status + ': ' + xhr.responseText; | |
if (xhr === this.activeRequest) { | |
this.error = response; | |
} | |
this.fire('core-error', {response: response, xhr: xhr}); | |
}, | |
complete: function(xhr) { | |
this.fire('core-complete', {response: xhr.status, xhr: xhr}); | |
}, | |
evalResponse: function(xhr) { | |
return this[(this.handleAs || 'text') + 'Handler'](xhr); | |
}, | |
xmlHandler: function(xhr) { | |
return xhr.responseXML; | |
}, | |
textHandler: function(xhr) { | |
return xhr.responseText; | |
}, | |
jsonHandler: function(xhr) { | |
var r = xhr.responseText; | |
try { | |
return JSON.parse(r); | |
} catch (x) { | |
console.warn('core-ajax caught an exception trying to parse response as JSON:'); | |
console.warn('url:', this.url); | |
console.warn(x); | |
return r; | |
} | |
}, | |
documentHandler: function(xhr) { | |
return xhr.response; | |
}, | |
blobHandler: function(xhr) { | |
return xhr.response; | |
}, | |
arraybufferHandler: function(xhr) { | |
return xhr.response; | |
}, | |
urlChanged: function() { | |
if (!this.handleAs) { | |
var ext = String(this.url).split('.').pop(); | |
switch (ext) { | |
case 'json': | |
this.handleAs = 'json'; | |
break; | |
} | |
} | |
this.autoGo(); | |
}, | |
paramsChanged: function() { | |
this.autoGo(); | |
}, | |
autoChanged: function() { | |
this.autoGo(); | |
}, | |
// TODO(sorvell): multiple side-effects could call autoGo | |
// during one micro-task, use a job to have only one action | |
// occur | |
autoGo: function() { | |
if (this.auto) { | |
this.goJob = this.job(this.goJob, this.go, 0); | |
} | |
}, | |
/** | |
* Performs an Ajax request to the specified URL. | |
* | |
* @method go | |
*/ | |
go: function() { | |
var args = this.xhrArgs || {}; | |
// TODO(sjmiles): we may want XHR to default to POST if body is set | |
args.body = this.body || args.body; | |
args.params = this.params || args.params; | |
if (args.params && typeof(args.params) == 'string') { | |
args.params = JSON.parse(args.params); | |
} | |
args.headers = this.headers || args.headers || {}; | |
if (args.headers && typeof(args.headers) == 'string') { | |
args.headers = JSON.parse(args.headers); | |
} | |
var hasContentType = Object.keys(args.headers).some(function (header) { | |
return header.toLowerCase() === 'content-type'; | |
}); | |
if (!hasContentType && this.contentType) { | |
args.headers['Content-Type'] = this.contentType; | |
} | |
if (this.handleAs === 'arraybuffer' || this.handleAs === 'blob' || | |
this.handleAs === 'document') { | |
args.responseType = this.handleAs; | |
} | |
args.withCredentials = this.withCredentials; | |
args.callback = this.receive.bind(this); | |
args.url = this.url; | |
args.method = this.method; | |
this.response = this.error = null; | |
this.activeRequest = args.url && this.xhr.request(args); | |
return this.activeRequest; | |
} | |
}); | |
</script> | |
</polymer-element> |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!-- | |
/** | |
* @group Polymer Core Elements | |
* | |
* core-xhr can be used to perform XMLHttpRequests. | |
* | |
* <core-xhr id="xhr"></core-xhr> | |
* ... | |
* this.$.xhr.request({url: url, params: params, callback: callback}); | |
* | |
* @element core-xhr | |
*/ | |
--> | |
<link rel="import" href="../polymer/polymer.html"> | |
<polymer-element name="core-xhr" hidden> | |
<script> | |
Polymer('core-xhr', { | |
/** | |
* Sends a HTTP request to the server and returns the XHR object. | |
* | |
* @method request | |
* @param {Object} inOptions | |
* @param {String} inOptions.url The url to which the request is sent. | |
* @param {String} inOptions.method The HTTP method to use, default is GET. | |
* @param {boolean} inOptions.sync By default, all requests are sent asynchronously. To send synchronous requests, set to true. | |
* @param {Object} inOptions.params Data to be sent to the server. | |
* @param {Object} inOptions.body The content for the request body for POST method. | |
* @param {Object} inOptions.headers HTTP request headers. | |
* @param {String} inOptions.responseType The response type. Default is 'text'. | |
* @param {boolean} inOptions.withCredentials Whether or not to send credentials on the request. Default is false. | |
* @param {Object} inOptions.callback Called when request is completed. | |
* @returns {Object} XHR object. | |
*/ | |
request: function(options) { | |
var xhr = new XMLHttpRequest(); | |
var url = options.url; | |
var method = options.method || 'GET'; | |
var async = !options.sync; | |
// | |
var params = this.toQueryString(options.params); | |
if (params && method == 'GET') { | |
url += (url.indexOf('?') > 0 ? '&' : '?') + params; | |
} | |
var xhrParams = this.isBodyMethod(method) ? (options.body || params) : null; | |
// | |
xhr.open(method, url, async); | |
if (options.responseType) { | |
xhr.responseType = options.responseType; | |
} | |
if (options.withCredentials) { | |
xhr.withCredentials = true; | |
} | |
this.makeReadyStateHandler(xhr, options.callback); | |
this.setRequestHeaders(xhr, options.headers); | |
xhr.send(xhrParams); | |
if (!async) { | |
xhr.onreadystatechange(xhr); | |
} | |
return xhr; | |
}, | |
toQueryString: function(params) { | |
var r = []; | |
for (var n in params) { | |
var v = params[n]; | |
n = encodeURIComponent(n); | |
r.push(v == null ? n : (n + '=' + encodeURIComponent(v))); | |
} | |
return r.join('&'); | |
}, | |
isBodyMethod: function(method) { | |
return this.bodyMethods[(method || '').toUpperCase()]; | |
}, | |
bodyMethods: { | |
POST: 1, | |
PUT: 1, | |
DELETE: 1 | |
}, | |
makeReadyStateHandler: function(xhr, callback) { | |
xhr.onreadystatechange = function() { | |
if (xhr.readyState == 4) { | |
callback && callback.call(null, xhr.response, xhr); | |
} | |
}; | |
}, | |
setRequestHeaders: function(xhr, headers) { | |
if (headers) { | |
for (var name in headers) { | |
xhr.setRequestHeader(name, headers[name]); | |
} | |
} | |
} | |
}); | |
</script> | |
</polymer-element> |
<!doctype html> | |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<html> | |
<head> | |
<title>core-ajax</title> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="core-ajax.html"> | |
</head> | |
<body> | |
<core-ajax auto url="http://gdata.youtube.com/feeds/api/videos/" | |
params='{"alt":"json", "q":"chrome"}' | |
handleAs="json"></core-ajax> | |
<template repeat="{{response.feed.entry}}"> | |
<div>{{title.$t}}</div> | |
</template> | |
<script> | |
document.addEventListener('polymer-ready', function() { | |
var ajax = document.querySelector("core-ajax"); | |
ajax.addEventListener("core-response", | |
function(e) { | |
document.querySelector('template').model = { | |
response: e.detail.response | |
}; | |
} | |
); | |
}); | |
</script> | |
</body> | |
</html> |
<!doctype html> | |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS | |
--> | |
<html> | |
<head> | |
<script src="../platform/platform.js"></script> | |
<link rel="import" href="../core-component-page/core-component-page.html"> | |
</head> | |
<body unresolved> | |
<core-component-page></core-component-page> | |
</body> | |
</html> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<x-meta id="core-ajax" label="Ajax" group="Core"> | |
<property name="handleAs" kind="select" options="json,text,xml,arraybuffer,blob,document"></property> | |
<property name="method" kind="select" options="GET,POST,PUT,DELETE"></property> | |
<template> | |
<core-ajax handleAs="json" method="GET"></core-ajax> | |
</template> | |
<template id="imports"> | |
<link rel="import" href="core-ajax.html"> | |
</template> | |
</x-meta> |
See the component page for more information.
<!doctype html> | |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<html> | |
<head> | |
<title>core-ajax-response-and-error</title> | |
<script src="../../../platform/platform.js"></script> | |
<script src="../../../polymer-test-tools/chai/chai.js"></script> | |
<script src="../../../polymer-test-tools/htmltest.js"></script> | |
<link rel="import" href="../../core-ajax.html"> | |
</head> | |
<body> | |
<polymer-element name="race-condition"> | |
<template> | |
<style>section {margin-top: 20px;}</style> | |
<core-ajax | |
id='ajax' | |
url="http://httpbin.org/delay/{{delay}}" | |
handleas="json" auto | |
response="{{response}}" | |
on-core-response="{{handleResponse}}"></core-ajax> | |
<div>Response url: {{response.url}}</div> | |
<div>Result: {{testResult}}</div> | |
<section> | |
<div>Requests</div> | |
<ul> | |
<template repeat='{{request in requests}}'> | |
<li> | |
{{request.delay}} second delay, Status: {{request.statusText}} | |
</li> | |
</template> | |
</ul> | |
</section> | |
</template> | |
<script> | |
Polymer({ | |
delay: 1, | |
response: null, | |
testResult: 'pending...', | |
passed: false, | |
requests: [], | |
observe: { | |
'$.ajax.activeRequest': 'requestChanged' | |
}, | |
domReady: function() { | |
setTimeout(function() { | |
if (this.response != null) { | |
console.error('HTTP request returned too quick!') | |
chai.assert.fail( | |
'', '', 'Indeterminate, initial request returned too quick'); | |
this.testResult = 'indeterminate'; | |
return; | |
} | |
this.delay = 2; | |
}.bind(this), 100); | |
// This will fail the test if it neither passes nor fails in time. | |
this.finalTimeout = setTimeout(function() { | |
chai.assert.fail('', '', 'Test timed out.'); | |
}, 7000); | |
}, | |
responseChanged: function() { | |
if (this.response.url != this.$.ajax.url) { | |
this.testResult = 'FAIL'; | |
chai.assert.fail(this.$.ajax.url, this.response.url, | |
'Race condition in response attribute'); | |
return; | |
} | |
this.passed = true; | |
}, | |
passedChanged: function() { | |
if (this.passed && this.testResult == 'pending...') { | |
this.testResult = 'PASS'; | |
clearTimeout(this.finalTimeout); | |
done(); | |
} | |
}, | |
requestChanged: function(o, n) { | |
this.requests.push({ | |
'statusText': 'pending', | |
xhr: n, | |
delay: this.delay | |
}); | |
}, | |
handleResponse: function(resp) { | |
var xhr = resp.detail.xhr; | |
for (var i = 0; i < this.requests.length; i++) { | |
if (this.requests[i].xhr === xhr) { | |
this.requests[i].statusText = xhr.statusText; | |
} | |
} | |
}, | |
}); | |
</script> | |
</polymer-element> | |
<race-condition></race-condition> | |
</body> | |
</html> |
<!doctype html> | |
<!-- | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<html> | |
<head> | |
<title>core-ajax</title> | |
<script src="../../../platform/platform.js"></script> | |
<script src="../../../polymer-test-tools/chai/chai.js"></script> | |
<script src="../../../polymer-test-tools/htmltest.js"></script> | |
<link rel="import" href="../../core-ajax.html"> | |
</head> | |
<body> | |
<core-ajax | |
url="http://gdata.youtube.com/feeds/api/videos/" | |
params='{"alt":"json", "q":"chrome"}' | |
handleAs="json" | |
auto></core-ajax> | |
<script> | |
document.addEventListener('polymer-ready', function() { | |
var assert = chai.assert; | |
var ajax = document.querySelector('core-ajax'); | |
ajax.addEventListener("core-response", function(event) { | |
assert.isTrue(event.detail.response.feed.entry.length > 0); | |
done(); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
htmlSuite('core-ajax', function() { | |
htmlTest('html/core-ajax.html'); | |
htmlTest('html/core-ajax-response-and-error.html'); | |
}); |
<!doctype htmlz> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> | |
<title>Web Component Test Runner</title> | |
<script src="../../polymer-test-tools/ci-support.js"></script> | |
</head> | |
<body> | |
<script> | |
runTests('tests.json'); | |
</script> | |
</body> | |
</html> |
{ | |
"tools": ["chai", "mocha-tdd"], | |
"tests": [ | |
"js/htmltests.js" | |
] | |
} |
{ | |
"name": "core-animated-pages", | |
"private": true, | |
"dependencies": { | |
"polymer": "Polymer/polymer#^0.4.0", | |
"core-selector": "Polymer/core-selector#^0.4.0", | |
"core-style": "Polymer/core-style#^0.4.0", | |
"core-transition": "Polymer/core-transition#^0.4.0" | |
}, | |
"homepage": "https://github.com/Polymer/core-animated-pages", | |
"version": "0.4.2", | |
"_release": "0.4.2", | |
"_resolution": { | |
"type": "version", | |
"tag": "0.4.2", | |
"commit": "224eb03b59d249ee6829946233546c1dd9d264b0" | |
}, | |
"_source": "git://github.com/Polymer/core-animated-pages.git", | |
"_target": "^0.4.0", | |
"_originalSource": "Polymer/core-animated-pages" | |
} |
{ | |
"name": "core-animated-pages", | |
"private": true, | |
"dependencies": { | |
"polymer": "Polymer/polymer#^0.4.0", | |
"core-selector": "Polymer/core-selector#^0.4.0", | |
"core-style": "Polymer/core-style#^0.4.0", | |
"core-transition": "Polymer/core-transition#^0.4.0" | |
} | |
} |
/* | |
* @license | |
* Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
* This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
* The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
* The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
* Code distributed by Google as part of the polymer project is also | |
* subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
*/ | |
:host { | |
display: block; | |
position: relative; | |
} | |
polyfill-next-selector { content: ':host > *'; } | |
::content > * { | |
position: absolute; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
} | |
polyfill-next-selector { content: ':host > *:not(.core-selected):not([animate])'; } | |
::content > *:not(.core-selected):not([animate]) { | |
display: none !important; | |
} |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<link href="../core-selector/core-selector.html" rel="import"> | |
<link href="transitions/hero-transition.html" rel="import"> | |
<link href="transitions/cross-fade.html" rel="import"> | |
<!-- | |
`core-animated-pages` selects one of its children "pages" to show and runs a transition | |
when switching between them. The transitions are designed to be pluggable, and can | |
accept any object that is an instance of a `core-transition-pages`. Transitions to run | |
are specified in the `transitions` attribute as a space-delimited string of `id`s of | |
transition elements. Several transitions are available with `core-animated-pages` by | |
default, including `hero-transition`, `cross-fade`, and `tile-cascade`. | |
Example: | |
<style> | |
#hero1 { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 300px; | |
height: 300px; | |
background-color: orange; | |
} | |
#hero2 { | |
position: absolute; | |
top: 200px; | |
left: 300px; | |
width: 300px; | |
height: 300px; | |
background-color: orange; | |
} | |
#bottom1, #bottom2 { | |
position: absolute; | |
bottom: 0; | |
top: 0; | |
left: 0; | |
height: 50px; | |
} | |
#bottom1 { | |
background-color: blue; | |
} | |
#bottom2 { | |
background-color: green; | |
} | |
</style> | |
// hero-transition and cross-fade are declared elsewhere | |
<core-animated-pages transitions="hero-transition cross-fade"> | |
<section id="page1"> | |
<div id="hero1" hero-id="hero" hero></div> | |
<div id="bottom1" cross-fade></div> | |
</section> | |
<section id="page2"> | |
<div id="hero2" hero-id="hero" hero></div> | |
<div id="bottom2" cross-fade></div> | |
</section> | |
</core-animated-pages> | |
In the above example, two transitions (`hero-transition` and `cross-fade`) are run when switching | |
between `page1` and `page2`. `hero-transition` transforms elements with the same `hero-id` such | |
that they appear to be shared across different pages. `cross-fade` fades out the elements marked | |
`cross-fade` in the outgoing page, and fades in those in the incoming page. See the individual | |
transition's documentation for specific details. | |
Finding elements to transition | |
------------------------------ | |
In general, a transition is applied to elements marked with a certain attribute. For example, | |
`hero-transition` applies the transition on elements with the `hero` and `hero-id` attribute. | |
Among the transitions included with `core-animated-pages`, script-based transitions such as | |
`hero-transition` generally look for elements up to one level of shadowRoot from the | |
`core-animated-pages` element, and CSS-based transitions such as `cross-fade` look for elements | |
within any shadowRoot within the `core-animated-pages` element. This means you can use | |
custom elements as pages and mark elements in their shadowRoots as heroes, or mark | |
elements in deeper shadowRoots with other transitions. | |
Example: | |
<polymer-element name="x-el" noscript> | |
<template> | |
<style> | |
#hero { | |
position: absolute; | |
top: 0; | |
right: 0; | |
width: 50px; | |
height: 300px; | |
background-color: blue; | |
} | |
</style> | |
<div id="hero" hero-id="bar" hero></div> | |
</template> | |
</polymer-element> | |
<polymer-element name="x-page-1" noscript> | |
<template> | |
<style> | |
#hero1 { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 300px; | |
height: 300px; | |
background-color: orange; | |
} | |
</style> | |
<div id="hero1" hero-id="foo" hero></div> | |
<div id="hero2" hero-id="bar" hero></div> | |
</template> | |
</polymer-element> | |
<polymer-element name="x-page-2" noscript> | |
<template> | |
<style> | |
#hero1 { | |
position: absolute; | |
top: 200px; | |
left: 300px; | |
width: 300px; | |
height: 300px; | |
background-color: orange; | |
} | |
#hero2 { | |
background-color: blue; | |
height: 150px; | |
width: 400px; | |
} | |
</style> | |
// The below element is one level of shadow from the core-animated-pages and will | |
// be transitioned. | |
<div id="hero1" hero-id="foo" hero></div> | |
// The below element contains a hero inside its shadowRoot making it two levels away | |
// from the core-animated-pages, and will not be transitioned. | |
<x-el></x-el> | |
</template> | |
</polymer-element> | |
<core-animated-pages transitions="hero-transition"> | |
<x-page-1></x-page-1> | |
<x-page-2></x-page-2> | |
</core-animated-pages> | |
Note that the container element of the page does not participate in the transition. | |
// This does not work | |
<core-animated-pages transitions="cross-fade"> | |
<section cross-fade></section> | |
<section cross-fade></section> | |
</core-animated-pages> | |
// This works | |
<core-animated-pages transitions="cross-fade"> | |
<section> | |
<div cross-fade></div> | |
</section> | |
<section> | |
<div cross-fade></div> | |
</section> | |
</core-animated-pages> | |
Dynamically setting up transitions | |
---------------------------------- | |
An easy way to set up transitions dynamically is to use property binding on | |
the transition attributes. | |
Example: | |
<core-animated-pages selected="{{selected}}"> | |
<section id="page1"> | |
<div hero-id="hero" hero></div> | |
</section> | |
<section id="page2"> | |
<div id="foo" hero-id="hero" hero?="{{selected === 1 || selected === 0}}" cross-fade="{{selected === 2}}"></div> | |
</section> | |
<section id="page3"> | |
</section> | |
</core-animated-pages> | |
In the above example, the "foo" element only behaves as a hero element if transitioning between | |
`#page1` and `#page2`. It gets cross-faded when transition to or from `#page3`. | |
Nesting pages | |
------------- | |
It is possible to nest core-animated-pages elements for organization. Excessive nesting is | |
not encouraged, however, since it makes setting up the transition more complex. | |
To nest core-animated-pages, the page containing the nested core-animated-pages element should | |
have a `selectedItem` property bound to the `selectedItem` property of the nested element. This | |
will allow the outer core-animated-pages to know which nested page it is actually transitioning | |
to. | |
Example: | |
<polymer-element name="nested-page" attributes="selectedItem"> | |
<template> | |
<core-animated-pages selectedItem="{{selectedItem}}"> | |
... | |
</core-animated-pages> | |
</template> | |
</polymer-element> | |
<core-animated-pages> | |
<section id="page1"></section> | |
<nested-page id="page2"></nested-page> | |
</core-animated-pages> | |
@element core-animated-pages | |
@extends core-selector | |
@status beta | |
@homepage github.io | |
--> | |
<!-- | |
Fired before a page transition occurs. Both pages involved in the transition are visible when | |
this event fires. This is useful if there is something the client needs to do when a page becomes | |
visible. | |
@event core-animated-pages-transition-prepare | |
--> | |
<!-- | |
Fired when a page transition completes. | |
@event core-animated-pages-transition-end | |
--> | |
<polymer-element name="core-animated-pages" extends="core-selector" notap attributes="transitions"> | |
<template> | |
<link href="core-animated-pages.css" rel="stylesheet"> | |
<shadow></shadow> | |
</template> | |
<script> | |
Polymer({ | |
eventDelegates: { | |
'core-transitionend': 'transitionEnd' | |
}, | |
/** | |
* A space-delimited string of transitions to use when switching between pages in this element. | |
* The strings are `id`s of `core-transition-pages` elements included elsewhere. See the | |
* individual transition's document for specific details. | |
* | |
* @attribute transitions | |
* @type string | |
* @default '' | |
*/ | |
transitions: '', | |
selected: 0, | |
/** | |
* The last page selected. This property is useful to dynamically set transitions based | |
* on incoming and outgoing pages. | |
* | |
* @attribute lastSelected | |
* @type Object | |
* @default null | |
*/ | |
lastSelected: null, | |
registerCallback: function() { | |
this.tmeta = document.createElement('core-transition'); | |
}, | |
created: function() { | |
this._transitions = []; | |
this.transitioning = []; | |
}, | |
transitionsChanged: function() { | |
this._transitions = this.transitions.split(' '); | |
}, | |
_transitionsChanged: function(old) { | |
if (this._transitionElements) { | |
this._transitionElements.forEach(function(t) { | |
t.teardown(this); | |
}, this); | |
} | |
this._transitionElements = []; | |
this._transitions.forEach(function(transitionId) { | |
var t = this.getTransition(transitionId); | |
if (t) { | |
this._transitionElements.push(t); | |
t.setup(this); | |
} | |
}, this); | |
}, | |
getTransition: function(transitionId) { | |
return this.tmeta.byId(transitionId); | |
}, | |
selectionSelect: function(e, detail) { | |
this.updateSelectedItem(); | |
// Wait to call applySelection when we run the transition | |
}, | |
applyTransition: function(src, dst) { | |
if (this.animating) { | |
this.cancelAsync(this.animating); | |
this.animating = null; | |
} | |
Platform.flush(); | |
if (this.transitioning.indexOf(src) === -1) { | |
this.transitioning.push(src); | |
} | |
if (this.transitioning.indexOf(dst) === -1) { | |
this.transitioning.push(dst); | |
} | |
// force src, dst to display | |
src.setAttribute('animate', ''); | |
dst.setAttribute('animate', ''); | |
// | |
var options = { | |
src: src, | |
dst: dst, | |
easing: 'cubic-bezier(0.4, 0, 0.2, 1)' | |
} | |
// fire an event so clients have a chance to do something when the | |
// new page becomes visible but before it draws. | |
this.fire('core-animated-pages-transition-prepare'); | |
// | |
// prepare transition | |
this._transitionElements.forEach(function(transition) { | |
transition.prepare(this, options); | |
}, this); | |
// | |
// force layout! | |
src.offsetTop; | |
// | |
// apply selection | |
this.applySelection(dst, true); | |
this.applySelection(src, false); | |
// | |
// start transition | |
this._transitionElements.forEach(function(transition) { | |
transition.go(this, options); | |
}, this); | |
if (!this._transitionElements.length) { | |
this.complete(); | |
} else { | |
this.animating = this.async(this.complete.bind(this), null, 5000); | |
} | |
}, | |
complete: function() { | |
if (this.animating) { | |
this.cancelAsync(this.animating); | |
this.animating = null; | |
} | |
this.transitioning.forEach(function(t) { | |
t.removeAttribute('animate'); | |
}); | |
this.transitioning = []; | |
this._transitionElements.forEach(function(transition) { | |
transition.ensureComplete(this); | |
}, this); | |
this.fire('core-animated-pages-transition-end'); | |
}, | |
transitionEnd: function(e) { | |
if (this.transitioning.length) { | |
var completed = true; | |
this._transitionElements.forEach(function(transition) { | |
if (!transition.completed) { | |
completed = false; | |
} | |
}); | |
if (completed) { | |
this.job('transitionWatch', function() { | |
this.complete(); | |
}, 100); | |
} | |
} | |
}, | |
selectedChanged: function(old) { | |
this.lastSelected = old; | |
this.super(arguments); | |
}, | |
selectedItemChanged: function(oldItem) { | |
this.super(arguments); | |
if (!oldItem) { | |
this.applySelection(this.selectedItem, true); | |
return; | |
} | |
if (this.hasAttribute('no-transition') || !this._transitionElements || !this._transitionElements.length) { | |
this.applySelection(oldItem, false); | |
this.applySelection(this.selectedItem, true); | |
return; | |
} | |
if (oldItem && this.selectedItem) { | |
// TODO(sorvell): allow bindings to update first? | |
var self = this; | |
Platform.flush(); | |
Platform.endOfMicrotask(function() { | |
self.applyTransition(oldItem, self.selectedItem); | |
}); | |
} | |
} | |
}); | |
</script> | |
</polymer-element> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<a href="demos/simple.html">pluggable transitions</a> | |
<br> | |
<a href="demos/news.html">icon to top bar</a> | |
<br> | |
<a href="demos/music.html">chip to card</a> | |
<br> | |
<a href="demos/list.html">list reorder</a> | |
<br> | |
<a href="demos/grid.html">grid to full screen</a> | |
<br> | |
<a href="demos/nested.html">nested core-animated-pages</a> | |
<br> | |
<a href="demos/quiz1.html">quiz: category to splash to question</a> | |
<br> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!doctype html> | |
<html> | |
<head> | |
<script src="../../platform/platform.js"></script> | |
<link href="../../core-icons/core-icons.html" rel="import"> | |
<link href="../../core-icon-button/core-icon-button.html" rel="import"> | |
<link href="../../core-toolbar/core-toolbar.html" rel="import"> | |
<link href="../core-animated-pages.html" rel="import"> | |
<style> | |
body { | |
font-family: sans-serif; | |
} | |
.toolbar { | |
background-color: steelblue; | |
} | |
#container { | |
overflow: auto; | |
} | |
.card { | |
position: relative; | |
height: 150px; | |
width: 150px; | |
font-size: 50px; | |
margin: 8px; | |
background-color: tomato; | |
border-radius: 4px; | |
cursor: default; | |
} | |
.view { | |
font-size: 250px; | |
background-color: tomato; | |
} | |
</style> | |
</head> | |
<body unresolved fullbleed vertical layout> | |
<template is="auto-binding"> | |
<core-toolbar class="toolbar"> | |
<core-icon-button icon="{{$.pages.selected != 0 ? 'arrow-back' : 'menu'}}" on-tap="{{back}}"></core-icon-button> | |
<div flex>Stuff</div> | |
<core-icon-button icon="more-vert"></core-icon-button> | |
</core-toolbar> | |
<core-animated-pages id="pages" flex selected="0" on-core-animated-pages-transition-end="{{transitionend}}" transitions="cross-fade-all hero-transition"> | |
<section vertical layout> | |
<div id="container" flex horizontal wrap around-justified layout hero-p> | |
<template repeat="{{item in items}}"> | |
<div class="card" vertical center center-justified layout hero-id="item-{{item}}" hero?="{{$.pages.selected === item + 1 || lastSelected === item + 1}}" on-tap="{{selectView}}"><span cross-fade>{{item}}</span></div> | |
</template> | |
</div> | |
</section> | |
<template repeat="{{item in items}}"> | |
<section vertical layout> | |
<div class="view" flex vertical center center-justified layout hero-id="item-{{item}}" hero?="{{$.pages.selected === item + 1 || $.pages.selected === 0}}"><span cross-fade>{{item}}</span></div> | |
</section> | |
</template> | |
</core-animated-pages> | |
</template> | |
<script> | |
addEventListener('template-bound', function(e) { | |
var scope = e.target; | |
var items = [], count=50; | |
for (var i=0; i < count; i++) { | |
items.push(i); | |
} | |
scope.items = items; | |
scope.selectView = function(e) { | |
var i = e.target.templateInstance.model.item; | |
this.$.pages.selected = i+1; | |
} | |
scope.back = function() { | |
this.lastSelected = this.$.pages.selected; | |
console.log(this.lastSelected); | |
this.$.pages.selected = 0; | |
} | |
scope.transitionend = function() { | |
if (this.lastSelected) { | |
this.lastSelected = null; | |
} | |
} | |
}) | |
</script> | |
</body> | |
</html> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!doctype html> | |
<html> | |
<head> | |
<script src="../../platform/platform.js"></script> | |
<link href="../core-animated-pages.html" rel="import"> | |
<style> | |
body { | |
font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
margin: 0; | |
} | |
</style> | |
</head> | |
<body unresolved> | |
<polymer-element name="list-demo"> | |
<template> | |
<style> | |
p { | |
margin: 8px; | |
} | |
.item { | |
background: #e7e7e7; | |
padding: 16px; | |
margin: 8px; | |
border-radius: 3px; | |
box-sizing: border-box; | |
position: relative; | |
} | |
</style> | |
<p>Tap to move to top</p> | |
<core-animated-pages id="pages" on-tap="{{reorder}}" selected="{{selected}}" on-core-animated-pages-transition-end="{{done}}" transitions="hero-transition"> | |
<section> | |
<template repeat="{{items}}"> | |
<div hero-id="{{h}}" hero class="item">{{v}}</div> | |
</template> | |
</section> | |
<section> | |
<template repeat="{{items2}}"> | |
<div hero-id="{{h}}" hero class="item">{{v}}</div> | |
</template> | |
</section> | |
</core-animated-pages> | |
</template> | |
<script> | |
Polymer('list-demo', { | |
selected: 0, | |
items: [ | |
{h: 'matt', v: 'Matt McNulty'}, | |
{h: 'scott', v: 'Scott Miles'}, | |
{h: 'steve', v: 'Steve Orvell'}, | |
{h: 'frankie', v: 'Frankie Fu'}, | |
{h: 'daniel', v: 'Daniel Freedman'}, | |
{h: 'yvonne', v: 'Yvonne Yip'}, | |
], | |
items2: [ | |
{h: 'matt', v: 'Matt McNulty'}, | |
{h: 'scott', v: 'Scott Miles'}, | |
{h: 'steve', v: 'Steve Orvell'}, | |
{h: 'frankie', v: 'Frankie Fu'}, | |
{h: 'daniel', v: 'Daniel Freedman'}, | |
{h: 'yvonne', v: 'Yvonne Yip'}, | |
], | |
reorder: function(e) { | |
if (this.$.pages.transitioning.length) { | |
return; | |
} | |
this.lastMoved = e.target; | |
this.lastMoved.style.zIndex = 10005; | |
var item = e.target.templateInstance.model; | |
var items = this.selected ? this.items : this.items2; | |
var i = this.selected ? this.items2.indexOf(item) : this.items.indexOf(item); | |
if (i != 0) { | |
items.splice(0, 0, item); | |
items.splice(i + 1, 1); | |
} | |
this.lastIndex = i; | |
this.selected = this.selected ? 0 : 1; | |
}, | |
done: function() { | |
var i = this.lastIndex; | |
var items = this.selected ? this.items : this.items2; | |
var item = items[i]; | |
items.splice(0, 0, item); | |
items.splice(i + 1, 1); | |
this.lastMoved.style.zIndex = null; | |
} | |
}); | |
</script> | |
</polymer-element> | |
<list-demo></list-demo> | |
</body> | |
</html> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!doctype html> | |
<html> | |
<head> | |
<title>core-animated-pages</title> | |
<script src="../../platform/platform.js"></script> | |
<link href="../core-animated-pages.html" rel="import"> | |
<style> | |
body { | |
font-family: 'Roboto 2', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
margin: 0; | |
background: #f1f1f1; | |
} | |
.green { | |
position: absolute; | |
top: 0; | |
right: 0; | |
left: 0; | |
height: 350px; | |
background: #70c26f; | |
} | |
</style> | |
</head> | |
<body> | |
<polymer-element name="music-demo"> | |
<template> | |
<style> | |
.chip-container { | |
position: absolute; | |
top: 275px; | |
right: 0; | |
left: 0; | |
text-align: center; | |
} | |
.chip { | |
display: inline-block; | |
position: relative; | |
border-radius: 3px; | |
margin: 4px; | |
overflow: hidden; | |
text-align: start; | |
background-color: #fff; | |
box-shadow: 0 2px 10px 0 rgba(0, 0, 0, 0.16); | |
} | |
.chip-top { | |
width: 200px; | |
height: 200px; | |
} | |
.chip-bottom { | |
padding: 8px; | |
line-height: 1.5; | |
} | |
.chip-album-title { | |
font-weight: bold; | |
} | |
#details { | |
padding: 200px 10% 0; | |
} | |
.card { | |
height: 400px; | |
border-radius: 3px; | |
text-align: start; | |
overflow: hidden; | |
background: #fff; | |
box-shadow: 0 6px 20px 0 rgba(0, 0, 0, 0.19); | |
} | |
.card-left { | |
width: 400px; | |
} | |
.card-right { | |
padding: 24px; | |
} | |
.card-icon { | |
border-radius: 50%; | |
width: 60px; | |
height: 60px; | |
margin-right: 16px; | |
} | |
.card-album-title { | |
font-size: 2em; | |
} | |
</style> | |
<core-animated-pages selected="{{page}}" transitions="hero-transition" on-core-animated-pages-transition-end="{{complete}}"> | |
<section> | |
<div class="chip-container" hero-p on-tap="{{transition}}"> | |
<template repeat="{{items as item}}"> | |
<div class="chip" hero-id="{{item.artist}}-{{item.album}}" hero?="{{selectedAlbum === item }}"> | |
<div class="chip-top" style="background:{{item.color}};" hero-id="{{item.artist}}-{{item.album}}-art" hero?="{{selectedAlbum === item}}"></div> | |
<div class="chip-bottom"> | |
<div class="chip-album-title">{{item.album}}</div> | |
<div class="chip-artist">{{item.artist}}</div> | |
</div> | |
</div> | |
</template> | |
</div> | |
</section> | |
<section id="details"> | |
<div class="card" layout horizontal hero-id="{{selectedAlbum.artist}}-{{selectedAlbum.album}}" hero on-tap="{{transition}}"> | |
<div class="card-left" style="background:{{selectedAlbum.color}};" hero-id="{{selectedAlbum.artist}}-{{selectedAlbum.album}}-art" hero></div> | |
<div class="card-right" flex> | |
<div layout horizontal center> | |
<div> | |
<div class="card-icon" style="background:{{selectedAlbum.color}};"></div> | |
</div> | |
<div flex> | |
<div class="card-album-title">{{selectedAlbum.album}}</div> | |
<div class="card-album-artist">{{selectedAlbum.artist}}</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</section> | |
</core-animated-pages> | |
</template> | |
<script> | |
Polymer('music-demo', { | |
page: 0, | |
items: [ | |
{ artist: 'Tycho', album: 'Fragments', color: '#f4db33' }, | |
{ artist: 'Tycho', album: 'Past Prologue', color: '#972ff8' }, | |
{ artist: 'Tycho', album: 'Spectre', color: '#7dd6fe' }, | |
{ artist: 'Tycho', album: 'Awake', color: '#dc3c84' } | |
], | |
selectedAlbum: null, | |
transition: function(e) { | |
if (this.page === 0 && e.target.templateInstance.model.item) { | |
this.selectedAlbum = e.target.templateInstance.model.item; | |
this.page = 1; | |
} else { | |
this.page = 0; | |
} | |
} | |
}); | |
</script> | |
</polymer-element> | |
<div class="green"></div> | |
<music-demo></music-demo> | |
</body> | |
</html> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<link href="../../core-icons/core-icons.html" rel="import"> | |
<link href="../../core-icon-button/core-icon-button.html" rel="import"> | |
<link href="../core-animated-pages.html" rel="import"> | |
<polymer-element name="nested-animated-pages"> | |
<template> | |
<style> | |
:host { | |
display: block; | |
position: relative; | |
} | |
core-animated-pages { | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
} | |
.tall-toolbar { | |
box-sizing: border-box; | |
height: 240px; | |
} | |
.tall-toolbar.colored { | |
fill: #fff; | |
color: #fff; | |
} | |
.tall-toolbar [flex] { | |
font-size: 1.5em; | |
} | |
core-icon-button { | |
margin: 16px; | |
} | |
.body { | |
background-color: #f1f1f1; | |
} | |
.square { | |
position: absolute; | |
width: 150px; | |
height: 150px; | |
left: 16px; | |
top: 175px; | |
} | |
</style> | |
<core-animated-pages id="pages" selected="{{page}}" selectedItem="{{selectedItem}}" transitions="hero-transition" no-transition?="{{noTransition}}"> | |
<section id="page1" cross-fade> | |
<div class="tall-toolbar colored" style="background-color:orange;" layout vertical hero-id="thing" hero?="{{page === 0 || !noTransition}}"> | |
<div layout horizontal center> | |
<core-icon-button icon="clear" on-tap="{{back}}"></core-icon-button> | |
<div flex>One</div> | |
<core-icon-button icon="arrow-forward" on-tap="{{transition}}"></core-icon-button> | |
</div> | |
<div flex></div> | |
</div> | |
<div flex class="body"></div> | |
</section> | |
<section layout vertical id="page2" cross-fade> | |
<div class="tall-toolbar" layout vertical> | |
<div layout horizontal center> | |
<core-icon-button icon="clear" on-tap="{{back}}"></core-icon-button> | |
<div flex>Two</div> | |
<core-icon-button icon="arrow-forward" on-tap="{{transition}}"></core-icon-button> | |
</div> | |
<div flex></div> | |
</div> | |
<div flex class="body"></div> | |
<div class="square" style="background-color:orange;" hero-id="thing" hero?="{{page === 1 || !noTransition}}"></div> | |
</section> | |
</core-animated-pages> | |
</template> | |
<script> | |
Polymer({ | |
publish: { | |
page: {value: 0} | |
}, | |
selectedItem: null, | |
noTransition: true, | |
back: function() { | |
this.noTransition = true; | |
this.fire('nested-back'); | |
}, | |
transition: function() { | |
this.noTransition = false; | |
this.page = this.page === 0 ? 1 : 0; | |
} | |
}); | |
</script> | |
</polymer-element> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!doctype html> | |
<html> | |
<head> | |
<title>core-animated-pages</title> | |
<script src="../../platform/platform.js"></script> | |
<link href="nested-animated-pages.html" rel="import"> | |
<style> | |
body { | |
font-family: 'Roboto 2', 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
margin: 0; | |
background: #f1f1f1; | |
} | |
nested-demo { | |
display: block; | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
right: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<polymer-element name="nested-demo"> | |
<template> | |
<style> | |
core-animated-pages { | |
display: block; | |
position: absolute; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
right: 0; | |
} | |
section { | |
text-align: center; | |
padding-top: 250px; | |
} | |
.square { | |
display: inline-block; | |
margin: 8px; | |
padding: 8px; | |
width: 200px; | |
height: 200px; | |
background-color: orange; | |
color: #fff; | |
} | |
</style> | |
<core-animated-pages selected="{{page}}" transitions="hero-transition cross-fade"> | |
<section on-tap="{{transition}}" layout horizontal center-justified> | |
<div class="square" id="thing1" hero-id="thing" hero?="{{subpage === 0}}" cross-fade?="{{subpage !== 0}}">thing 1</div> | |
<div class="square" id="thing2" hero-id="thing" hero?="{{subpage === 1}}" cross-fade?="{{subpage !== 1}}">thing 2</div> | |
</section> | |
<nested-animated-pages page="{{subpage}}" on-nested-back="{{back}}"></nested-animated-pages> | |
</core-animated-pages> | |
</template> | |
<script> | |
Polymer('nested-demo', { | |
page: 0, | |
subpage: 0, | |
transition: function(e) { | |
var el = e.target; | |
if (el.id === "thing1") { | |
this.subpage = 0; | |
} else { | |
this.subpage = 1; | |
} | |
setTimeout(function() { | |
this.page = 1; | |
}.bind(this), 200); | |
}, | |
back: function() { | |
this.page = 0; | |
} | |
}); | |
</script> | |
</polymer-element> | |
<nested-demo></nested-demo> | |
</body> | |
</html> |
<!-- | |
@license | |
Copyright (c) 2014 The Polymer Project Authors. All rights reserved. | |
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt | |
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt | |
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt | |
Code distributed by Google as part of the polymer project is also | |
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt | |
--> | |
<!doctype html> | |
<html> | |
<head> | |
<script src="../../platform/platform.js"></script> | |
<link href="../../core-icons/core-icons.html" rel="import"> | |
<link href="../../core-icons/social-icons.html" rel="import"> | |
<link href="../../core-toolbar/core-toolbar.html" rel="import"> | |
<link href="../../paper-shadow/paper-shadow.html" rel="import"> | |
<link href="../core-animated-pages.html" rel="import"> | |
<style shim-shadowdom> | |
body { | |
font-family: Roboto, 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
margin: 0; | |
} | |
.fit { | |
position: absolute; | |
top: 0; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
width: 420px; | |
} | |
.toolbar { | |
background: #8d3efc; | |
/* FIXME */ | |
color: #fff !important; | |
fill: #fff; | |
} | |
.toolbar-2 { | |
position: absolute; | |
top: 0; | |
left: 0; | |
background: #000; | |
color: #fff; | |
text-align: center; | |
font-size: 48px; | |
} | |
body /deep/ .toolbar-2 { | |
position: absolute; | |
top: 0; | |
left: 0; | |
margin: 0; | |
width: 420px; | |
background: #000; | |
color: #fff; | |
text-align: center; | |
font-size: 48px; | |
} | |
.container { | |
background-color: #e7e7e7; | |
padding: 16px; | |
} | |
.card { | |
position: relative; | |
background-color: #fff; | |
border-radius: 2px; | |
} | |
.card-top { | |
background: #f2da2f; | |
height: 240px; | |
} | |
.card-top-2 { | |
background: #99f8b7; | |
height: 240px; | |
} | |
.card-bottom { | |
padding: 24px; | |
} | |
.headline { | |
font-size: 24px; | |
overflow: hidden; | |
text-overflow: ellipsis; | |
white-space: nowrap; | |
} | |
.icon { | |
position: relative; | |
background: #000; | |
color: #fff; | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
} | |
.icon::after { | |
content: 'T'; | |
font-size: 24px; | |
position: absolute; | |
top: 7px; | |
left: 13px; | |
} | |
.source-container { | |
margin-top: 16px; | |
} | |
.two-lines { | |
margin-left: 16px; | |
} | |
.source { | |
font-size: 14px; | |
} | |
.time { | |
color: rgba(0, 0, 0, 0.54); | |
font-size: 12px; | |
} | |
.tiles-container { | |
margin-top: 16px; | |
} | |
.tile { | |
position: relative; | |
display: inline-block; | |
width: 85px; | |
height: 85px; | |
background: #fff; | |
border-radius: 2px; | |
} | |
</style> | |
</head> | |
<body unresolved> | |
<polymer-element name="shadow-div" noscript> | |
<template> | |
<style> | |
:host { | |
display: block; | |
} | |
</style> | |
<paper-shadow target="{{}}" z="1"></paper-shadow> | |
<content></content> | |
</template> | |
</polymer-element> | |
<core-animated-pages class="fit" selected="0" transitions="cross-fade-all hero-transition"> | |
<section id="first"> | |
<core-toolbar class="tall toolbar"> | |
<core-icon icon="menu"></core-icon> | |
<div flex>Highlights</div> | |
<core-icon icon="social:share"></core-icon> | |
<core-icon icon="bookmark"></core-icon> | |
<core-icon icon="more-vert"></core-icon> | |
</core-toolbar> | |
<div class="container" hero-p> | |
<shadow-div class="card" hero-p onclick="stuff()"> | |
<div class="card-top"></div> | |
<div class="card-bottom" hero-p> | |
<div class="headline">Google's Craziest Offices</div> | |
<div class="source-container" hero-p layout horizontal center> | |
<div class="icon" hero-id="icon-header" hero></div> | |
<div class="two-lines"> | |
<div class="source">The New York Times</div> | |
<div class="time">36 minutes ago</div> | |
</div> | |
</div> | |
</div> | |
</shadow-div> | |
<div class="tiles-container" layout horizontal justified> | |
<shadow-div class="tile"></shadow-div> | |
<shadow-div class="tile"></shadow-div> | |
<shadow-div class="tile"></shadow-div> | |
<shadow-div class="tile"></shadow-div> | |
</div> | |
</div> | |
</section> | |
<section id="second"> | |
<core-toolbar class="tall" hero-p> | |
<core-toolbar class="tall toolbar-2" hero-id="icon-header" hero> | |
<div flex class="middle">T</div> | |
</core-toolbar> | |
</core-toolbar> | |
<div class="container"> | |
<shadow-div class="card" onclick="stuff()"> | |
<div class="card-top-2"></div> | |
<div class="card-bottom"> | |
<div class="headline">Some long overflowing headline</div> | |
<div class="source-container" layout horizontal center> | |
<div class="icon" style="background:red;"></div> | |
<div class="two-lines"> | |
<div class="source">The New York Times</div> | |
<div class="time">36 minutes ago</div> | |
</div> | |
</div> | |
</div> | |
</shadow-div> | |
<div class="tiles-container" layout horizontal justified> | |
<shadow-div class="tile"></shadow-div> | |
<shadow-div class="tile"></shadow-div> | |
<shadow-div class="tile"></shadow-div> | |
<shadow-div class="tile"></shadow-div> | |
</div> | |
</div> | |
</section> | |
</core-animated-pages> | |
<script> | |
function stuff(e) { | |
var p = document.querySelector('core-animated-pages'); | |
p.selected = p.selected ? 0 : 1; | |
} | |
</script> | |
</body> | |
</html> |
�PNG | |