The branch, master has been updated via 1f45e851f7520b56283a4da029a1150d1177a77a (commit) via c9ea04a046304f9968858bb10edca60dfbc9d462 (commit) via e6874ce0c71fe1a5d7d8f8be366f1f2443da4abc (commit) via 5ee9a1fe1a91319f5e3f377b2eb4b43e5c10e717 (commit) via f08b7d52ffc68e45ba78a024b0015ff4deb276e0 (commit) via 5d5285e2899dd3071c6a53879b8c358dc8b0609a (commit) via 21246070fa339ea87b3702aa11582eded2320b65 (commit) via 9db1342b2a6896e13f48a042618ec3ec09d9ced5 (commit) via 8d887f8dffc037af4e9a65635ca7d193e3291a0a (commit) via b3c575d443521be8169d73f25361e4c20929a7e2 (commit) via 2816b427ab4a9af286edfd44d7b27b61b52b107c (commit) via 496b3814916414eb91c9a583f6211683eaf2d6fe (commit) via 16749a04352f6da6e2635310d7289c1bd1328c86 (commit) via 5d6d0d2e68d4e239bd56dd0a5304fccd673202b7 (commit) via 5e0437cb2c575560cda883b8d799e21afbb9706f (commit) via d00ea89935b5a7052aeee81c2b3f277b935ea118 (commit) via da8124e689386bc53fc23cbcf87f087d7723c478 (commit) via 316f1c6d235a53ecb6050997ddcb632ec9cbfec3 (commit) via 3ce1e7f759daf957c7e0fcb55bc44d9f4d5646ee (commit) via f4a2a92ea24336aa13730188ea03dee7671808cf (commit) via 0f64babd3c49f69a985b445d1c06f365b570c84d (commit) via 2f156ee6ec5c048d78da1df28662fc95289a1b56 (commit) via 2bf553247b0efd42b7fbcf137a1367b71017b5fc (commit) via bccbff41aed68bfdabaf5578d862ba6fb2929d07 (commit) via b3c988157206c81c1c09c6720dbe8fe2797f98c7 (commit) via 659252cd71eab4e6f7f648c5339e6a5f0910a938 (commit) via 831da843c1f9d72d59f1fff8452a59188d9409bc (commit) via dc433429e870da823130d63f08e5055e7d4fc1af (commit) via 64a16e92023bdf4237280ffc27c6665396eccf0e (commit) via 21b65e56f4decdce8c634272c59329fe95a47136 (commit) via c928d53ef83438bbd1639a473dfcd4ecaa43101d (commit) via f4fa0d9f797d36ccf648f32dcf4738b09e1ec457 (commit) via 9410a4cfd9d92342e9c30ae3c43c64ceb45462b0 (commit) via 0d1872595f6f1c14602a7edd1d652bc84ffe0b3f (commit) via fcb3273c64d588f32b733aae0e8b95aa2d920523 (commit) via 902fc1d95da7c912993b893ce8f883569fb41da0 (commit) via e913dcc6e115a52b2a3305e53cc74fcc3d4d1a46 (commit) via 86323a781560b4526b5f5010a1c93adeb410ba51 (commit) via f47ce5dde3ba322f2045c3968354d951c36683f4 (commit) via 855532ea2eddd7f669a52adc6838e716e19fe902 (commit) via ed40e7f7bb6172607294c9a769ae7d5b4b6029b2 (commit) via 14001bcc3b8202636c29e1bb66fdff342e6107b2 (commit) via 775beaca2254615341624b5e3a4f047c4cb59be5 (commit) via cbc5a582b637d680054c88ef00bd9600e55daa91 (commit) via fed61fba72498d369e720c37ff0c9c0c72389d84 (commit) via 5e094266669b0ec306e7a836154373a52766fd98 (commit) via 9abc87d3a5915783f6e71ea56eefbce4c3196616 (commit) via fe9844cb138202257bc0ebf6c5aac3062555f0ca (commit) via 01511d1a9b6f93e1cb657c32a43aa314e42ec2a0 (commit) via e551c09604a3147b88ad5b8ef655f3563e110029 (commit) via 9fdbb850bb4bb6e13856d1124f5c360a8c02d892 (commit) via 207bfd47e30d812126751440dc5ff8d9cb46fb2b (commit) via 948b85825313818654df7d6c5907f488dbcd8efc (commit) via 349a650357bc0adacc139c3d5ffabfb0ad475bbd (commit) via 3b6e1926711aecfb2997b685edbb5613d9358c92 (commit) via 71cc8fec3d2942871251918f38453d9ee65699f9 (commit) via f76b91fdb1b9e77a58fcdce8145a99cc71a2fb2e (commit) via 8d5850674b426aa2401dbdef975d09e6c42f38a1 (commit) via 6938866d8175a2fe54f3f75038a1cea46a2e2797 (commit) via 226ca36c9cfa6f15b79ec058f92bc5c36675541f (commit) via cbaecced9fbf52e10e924f24d7ce3ca8e9512fc5 (commit) via 3adc27ec81a4eb4bd54465056d06009a7f410800 (commit) via d1d495fdda025859ff0e340c1b59b4145df29952 (commit) via 9385012b97b4c188618ee8658d4e7a3b9200aa5d (commit) via 3d62f95c89dbec80690fee0d92729317e5aff6c2 (commit) via b366c76f422f875d880a66b2c33cdfd3608ba0ef (commit) via 4885b174da8c9dca2b0bea9f4ea38fb63bc76eb9 (commit) via ba20375ba6c8461987902bd54139536c9d009215 (commit) via 58da05381ca6daf95d377c684f6f4b13aa467ec6 (commit) via 4a788b150596a84f34012cae1a5d62712e9b5bac (commit) via 99775c9bad552aac8b904996d673f1023821ab13 (commit) via b161f43ec9089e0d2d5fb8a619056e6f9577a380 (commit) via c0270352d8dbe5573589d95ef87aed5983f6fde7 (commit) via 1a4fc3b87901e7d1edaefb3681a9619e890ec9d6 (commit) via 7a82a65c4ecfd513448a1be9e1b11c77d51292be (commit) via b37f788a0bba35521a693da8651ff535daa56a25 (commit) via df9bcddd39e9fd66c0002bdd4e5496616dc5bc60 (commit) via c6c0b451a7e855460efb77b3f8e9bf92dceaf118 (commit) via cd17b9721b2a85363aad25be54a7e73232daaf88 (commit) via 573da119a4a238d1bbd2b675dbfdebd1d17f0188 (commit) via c3dda3823a09d6cac72cbb6bed72909b7cff5a3d (commit) via a0302c666c1bf9f9715364a6301238b474aedac1 (commit) via e52393015ffdfbfaf6941c6e0b8936750cede9ee (commit) via 9060108adccba661409ebd9cc7d6f59350cdc83f (commit) via 58f6523c57cac23ba5d2e0d41e254834b8c8963a (commit) via 00515b13ba5cf90d387f7fc7588987084412b7ab (commit) via d10e62ca37817c251cc3483b5d8ceea035759f9b (commit) via c0749d556378341727982e9ea08bb1de08e7c5ea (commit) via f8656fb0f8e9ecffa8abdc5dff1ab7c1b7e706db (commit) via 014a054c7f740aa6e172059ed440928685de6fd5 (commit) via 6d7358abb34a229bc138ec9381dceb1eedd6d498 (commit) via 0839be45a5ac3281a3e0bb863ae804780bb87d9d (commit) via 5ff65c4b54dfe8e0da176ca6f0cbb80cf3e0dfcf (commit) via 9dc2cbd19cf68807a0994e80a4c1edba049f21e0 (commit) from 8cd155d2e31d13d32f2779d15f7b39265657c50a (commit)
- Log ----------------------------------------------------------------- commit 1f45e851f7520b56283a4da029a1150d1177a77a Merge: 8cd155d2e31d13d32f2779d15f7b39265657c50a c9ea04a046304f9968858bb10edca60dfbc9d462 Author: Michal Čihař mcihar@novell.com Date: Tue Jun 14 13:49:12 2011 +0200
Merge remote-tracking branch 'tyron/master'
commit c9ea04a046304f9968858bb10edca60dfbc9d462 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 21:28:27 2011 +0200
Status variables refresh link once again broken
commit e6874ce0c71fe1a5d7d8f8be366f1f2443da4abc Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 18:53:45 2011 +0200
Clearer comment on PMA_formatNumber()
commit 5ee9a1fe1a91319f5e3f377b2eb4b43e5c10e717 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 18:14:57 2011 +0200
Assures that process table is below traffic/connection tables, not beside
commit f08b7d52ffc68e45ba78a024b0015ff4deb276e0 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 18:00:33 2011 +0200
3 more bugfixes
commit 5d5285e2899dd3071c6a53879b8c358dc8b0609a Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 17:59:58 2011 +0200
When exporting a chart as png, canvg renders hidden elements. In the case of pie chart it renders overlapping texts. This patch fixes this for text and path elements. This bug is also reported to the original canv author.
commit 21246070fa339ea87b3702aa11582eded2320b65 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 16:55:49 2011 +0200
Some style fixes mostly for original theme
commit 9db1342b2a6896e13f48a042618ec3ec09d9ced5 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 16:45:31 2011 +0200
Bug Fixes
commit 8d887f8dffc037af4e9a65635ca7d193e3291a0a Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 15:29:45 2011 +0200
git being a pain in the...
commit b3c575d443521be8169d73f25361e4c20929a7e2 Merge: 2816b427ab4a9af286edfd44d7b27b61b52b107c ca6f3b0ac27ab2124c196dda8484397d99656e0b Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 15:27:09 2011 +0200
Merge remote-tracking branch 'origin/master'
Conflicts: tbl_chart.php
commit 2816b427ab4a9af286edfd44d7b27b61b52b107c Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 15:00:11 2011 +0200
Merge remote-tracking branch 'origin/master'
commit 496b3814916414eb91c9a583f6211683eaf2d6fe Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 14:57:10 2011 +0200
hide json code
commit 16749a04352f6da6e2635310d7289c1bd1328c86 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 13 14:56:31 2011 +0200
2 more points in graphs
commit 5d6d0d2e68d4e239bd56dd0a5304fccd673202b7 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 21:15:22 2011 +0200
And another codeline missing :D
commit 5e0437cb2c575560cda883b8d799e21afbb9706f Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 21:13:24 2011 +0200
More old code
commit d00ea89935b5a7052aeee81c2b3f277b935ea118 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 19:55:05 2011 +0200
Fix: Reinitialize pie chart when refreshing the query tab
commit da8124e689386bc53fc23cbcf87f087d7723c478 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 19:44:33 2011 +0200
Fixed refresh in variables tab
commit 316f1c6d235a53ecb6050997ddcb632ec9cbfec3 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 19:36:30 2011 +0200
Connections and Traffic table side by side @server status page
commit 3ce1e7f759daf957c7e0fcb55bc44d9f4d5646ee Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 19:26:29 2011 +0200
Small improvements
commit f4a2a92ea24336aa13730188ea03dee7671808cf Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 19:26:03 2011 +0200
Fixed PMA_formatNumber() sometimes choosing too high SI preifx
commit 0f64babd3c49f69a985b445d1c06f365b570c84d Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 19:25:07 2011 +0200
Removed pChart references
commit 2f156ee6ec5c048d78da1df28662fc95289a1b56 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 16:06:51 2011 +0200
- SVG Export working - Smaller chart markers
commit 2bf553247b0efd42b7fbcf137a1367b71017b5fc Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 15:35:51 2011 +0200
Clear timeout once refresh rate has changed and display what the select list is for
commit bccbff41aed68bfdabaf5578d862ba6fb2929d07 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 15:17:52 2011 +0200
Configurable refresh rate for real-time charts in the status page
commit b3c988157206c81c1c09c6720dbe8fe2797f98c7 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 10 14:23:46 2011 +0200
Better realtime chart behaviour when the chart hasn't been filled with data yet
commit 659252cd71eab4e6f7f648c5339e6a5f0910a938 Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 20:08:38 2011 +0200
pChart is not needed anymore
commit 831da843c1f9d72d59f1fff8452a59188d9409bc Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 20:08:17 2011 +0200
Fixes of some bugs
commit dc433429e870da823130d63f08e5055e7d4fc1af Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 16:06:04 2011 +0200
Don't add duplicates to xaxis
commit 64a16e92023bdf4237280ffc27c6665396eccf0e Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 15:49:15 2011 +0200
undefined variable fix on single column result
commit 21b65e56f4decdce8c634272c59329fe95a47136 Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 15:36:41 2011 +0200
A currently rather strange series selection to behave similar to the old charting system
commit c928d53ef83438bbd1639a473dfcd4ecaa43101d Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 12:33:07 2011 +0200
Fix that single column results work too
commit f4fa0d9f797d36ccf648f32dcf4738b09e1ec457 Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 9 00:53:32 2011 +0200
- Dynamic chart with for the server status chart - Table chart replaced with a Highcharts generated chart that also behaves somewhat more intellegently
commit 9410a4cfd9d92342e9c30ae3c43c64ceb45462b0 Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 8 10:31:22 2011 +0200
Forgot 2 lines in PMA_formatNumber()
commit 0d1872595f6f1c14602a7edd1d652bc84ffe0b3f Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 8 10:19:59 2011 +0200
Math to the resuce! Nicer calculation of PMA_formatNumber() without any loops
commit fcb3273c64d588f32b733aae0e8b95aa2d920523 Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 8 01:53:21 2011 +0200
-Tabs +Spaces
commit 902fc1d95da7c912993b893ce8f883569fb41da0 Author: Tyron Madlener tyronx@gmail.com Date: Tue Jun 7 21:32:05 2011 +0200
- Replaced pChart profiling chart with highchart chart - Moved PMA_profilingResults() function code inline in sql.php as its used at only one place
commit e913dcc6e115a52b2a3305e53cc74fcc3d4d1a46 Author: Tyron Madlener tyronx@gmail.com Date: Tue Jun 7 20:58:56 2011 +0200
-Tabs +Spaces
commit 86323a781560b4526b5f5010a1c93adeb410ba51 Author: Tyron Madlener tyronx@gmail.com Date: Tue Jun 7 20:50:38 2011 +0200
Better PMA_formatNumber() behaviour.
commit f47ce5dde3ba322f2045c3968354d951c36683f4 Author: Tyron Madlener tyronx@gmail.com Date: Tue Jun 7 14:56:34 2011 +0200
Fixed refreshrate
commit 855532ea2eddd7f669a52adc6838e716e19fe902 Author: Tyron Madlener tyronx@gmail.com Date: Tue Jun 7 14:54:51 2011 +0200
replace pChart with highchart for the query statistics
commit ed40e7f7bb6172607294c9a769ae7d5b4b6029b2 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 15:46:01 2011 +0200
Small changes/fixes
commit 14001bcc3b8202636c29e1bb66fdff342e6107b2 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 15:29:35 2011 +0200
-tabs +spaces
commit 775beaca2254615341624b5e3a4f047c4cb59be5 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 15:26:06 2011 +0200
More secure chart export
commit cbc5a582b637d680054c88ef00bd9600e55daa91 Merge: fed61fba72498d369e720c37ff0c9c0c72389d84 f2fe4db5f1d100f29152445e4367789707007800 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 14:41:44 2011 +0200
Merge remote-tracking branch 'origin/master'
commit fed61fba72498d369e720c37ff0c9c0c72389d84 Merge: 5e094266669b0ec306e7a836154373a52766fd98 d14e49f8271c25b11cdc98962e6b24338aaef90e Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 14:38:58 2011 +0200
Merge remote-tracking branch 'origin/master'
commit 5e094266669b0ec306e7a836154373a52766fd98 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 14:38:56 2011 +0200
Merge
commit 9abc87d3a5915783f6e71ea56eefbce4c3196616 Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 13:50:17 2011 +0200
Connection / Process charting on the server traffic tab
commit fe9844cb138202257bc0ebf6c5aac3062555f0ca Author: Tyron Madlener tyronx@gmail.com Date: Mon Jun 6 13:16:35 2011 +0200
Small code improvements
commit 01511d1a9b6f93e1cb657c32a43aa314e42ec2a0 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 3 12:20:36 2011 +0200
Some commments
commit e551c09604a3147b88ad5b8ef655f3563e110029 Author: Tyron Madlener tyronx@gmail.com Date: Fri Jun 3 12:10:53 2011 +0200
js error fix when exporting a chart
commit 9fdbb850bb4bb6e13856d1124f5c360a8c02d892 Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 2 22:00:42 2011 +0200
Proof of concept for svg->canvas->image export of charts
commit 207bfd47e30d812126751440dc5ff8d9cb46fb2b Author: Tyron Madlener tyronx@gmail.com Date: Thu Jun 2 15:22:28 2011 +0200
- Fix: Hide refresh button only for current tab - Fix: Tablesorter sorts thousands-seperated values correctly
commit 948b85825313818654df7d6c5907f488dbcd8efc Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 1 18:34:43 2011 +0200
Default refreshrate 3sec => 5sec
commit 349a650357bc0adacc139c3d5ffabfb0ad475bbd Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 1 18:31:57 2011 +0200
clear timeouts properly
commit 3b6e1926711aecfb2997b685edbb5613d9358c92 Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 1 18:01:39 2011 +0200
- Don't loose "sortability" when switching between realtime and static data - Chart: display 0 as first value
commit 71cc8fec3d2942871251918f38453d9ee65699f9 Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 1 17:49:27 2011 +0200
- Query chart: - Point tooltip now shows what type of queries have been issued - Changed data from 'Seconds per Query' to 'Seconds since last refresh'
commit f76b91fdb1b9e77a58fcdce8145a99cc71a2fb2e Author: Tyron Madlener tyronx@gmail.com Date: Wed Jun 1 11:21:45 2011 +0200
Reinitalize tablesorter after refresh
commit 8d5850674b426aa2401dbdef975d09e6c42f38a1 Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 15:50:50 2011 +0200
Sorting indicators in the column headers
commit 6938866d8175a2fe54f3f75038a1cea46a2e2797 Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 15:50:03 2011 +0200
+source version, -minified version
commit 226ca36c9cfa6f15b79ec058f92bc5c36675541f Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 14:05:32 2011 +0200
Display 0 for actual 0 values
commit cbaecced9fbf52e10e924f24d7ce3ca8e9512fc5 Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 12:14:14 2011 +0200
Replaced minified highchart files with source files
commit 3adc27ec81a4eb4bd54465056d06009a7f410800 Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 12:03:57 2011 +0200
A bit better number formatting for the query statistics table
commit d1d495fdda025859ff0e340c1b59b4145df29952 Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 12:03:17 2011 +0200
Highcarts (unminivied) source files
commit 9385012b97b4c188618ee8658d4e7a3b9200aa5d Author: Tyron Madlener tyronx@gmail.com Date: Tue May 31 10:33:37 2011 +0200
If the PMA_formatNumber() parameters would result in 0.0, it displays <0.1 instead
commit 3d62f95c89dbec80690fee0d92729317e5aff6c2 Author: Tyron Madlener tyronx@gmail.com Date: Mon May 30 22:57:08 2011 +0200
Revert "timepicker.js is redundant, its already in the jquery ui js file"
This reverts commit a0302c666c1bf9f9715364a6301238b474aedac1.
commit b366c76f422f875d880a66b2c33cdfd3608ba0ef Merge: 4885b174da8c9dca2b0bea9f4ea38fb63bc76eb9 b5c1ee65092ebf67537931dfc1e396acfb1b10c2 Author: Tyron Madlener tyronx@gmail.com Date: Mon May 30 22:46:02 2011 +0200
Merge remote-tracking branch 'origin/master'
Conflicts: themes/pmahomme/css/theme_right.css.php
commit 4885b174da8c9dca2b0bea9f4ea38fb63bc76eb9 Author: Tyron Madlener tyronx@gmail.com Date: Mon May 30 22:37:18 2011 +0200
- Implemented more reusable code for realtime charting - added 'queries per second' chart in the query statistics tab - removed test chart - fixed kill process url
commit ba20375ba6c8461987902bd54139536c9d009215 Author: Tyron Madlener tyronx@gmail.com Date: Mon May 30 17:02:36 2011 +0200
Merged processes page into server status page
commit 58da05381ca6daf95d377c684f6f4b13aa467ec6 Author: Tyron Madlener tyronx@gmail.com Date: Mon May 30 17:01:09 2011 +0200
Added realtime charting of processes with the highcart lib for testing
commit 4a788b150596a84f34012cae1a5d62712e9b5bac Author: unknown <Tyron@.(none)> Date: Fri May 27 19:34:53 2011 +0200
Small adjustments
commit 99775c9bad552aac8b904996d673f1023821ab13 Author: unknown <Tyron@.(none)> Date: Fri May 27 19:30:13 2011 +0200
Don't show initials table for 20 privs or less
commit b161f43ec9089e0d2d5fb8a619056e6f9577a380 Author: unknown <Tyron@.(none)> Date: Fri May 27 19:25:33 2011 +0200
Don't show 5 second long success message when reloading privs
commit c0270352d8dbe5573589d95ef87aed5983f6fde7 Author: unknown <Tyron@.(none)> Date: Fri May 27 19:00:09 2011 +0200
Fixed expanded 'More menu' being behind jQuery tabs.
commit 1a4fc3b87901e7d1edaefb3681a9619e890ec9d6 Author: tyron tyronx@gmail.com Date: Tue May 24 20:48:21 2011 +0200
More wrong variable scoping
commit 7a82a65c4ecfd513448a1be9e1b11c77d51292be Author: tyron tyronx@gmail.com Date: Tue May 24 20:45:44 2011 +0200
Fixed wrong variable scoping
commit b37f788a0bba35521a693da8651ff535daa56a25 Author: tyron tyronx@gmail.com Date: Tue May 24 20:41:07 2011 +0200
Fix for bug #3242060. Total sum of queries sums up all Com_ vars instead of using the Questions var
commit df9bcddd39e9fd66c0002bdd4e5496616dc5bc60 Author: tyron tyronx@gmail.com Date: Tue May 24 19:30:42 2011 +0200
re-initialize ajax loaded tooltip
commit c6c0b451a7e855460efb77b3f8e9bf92dceaf118 Merge: cd17b9721b2a85363aad25be54a7e73232daaf88 84006bfeb21723a510d42dff929f5cf290b39d9b Author: tyron tyronx@gmail.com Date: Tue May 24 18:31:22 2011 +0200
Merge remote-tracking branch 'origin/master'
commit cd17b9721b2a85363aad25be54a7e73232daaf88 Author: tyron tyronx@gmail.com Date: Tue May 24 18:29:27 2011 +0200
Ajax refresh for all tabs now
commit 573da119a4a238d1bbd2b675dbfdebd1d17f0188 Merge: c3dda3823a09d6cac72cbb6bed72909b7cff5a3d 1835d93534d7e82b10c4e08805204b5d10459e63 Author: tyron tyronx@gmail.com Date: Tue May 24 16:18:01 2011 +0200
Merge remote-tracking branch 'origin/master'
commit c3dda3823a09d6cac72cbb6bed72909b7cff5a3d Author: tyron tyronx@gmail.com Date: Tue May 24 15:58:19 2011 +0200
Fixed caching bug, added ajax refresh for variables table
commit a0302c666c1bf9f9715364a6301238b474aedac1 Author: tyron tyronx@gmail.com Date: Tue May 24 15:56:55 2011 +0200
timepicker.js is redundant, its already in the jquery ui js file
commit e52393015ffdfbfaf6941c6e0b8936750cede9ee Author: tyron tyronx@gmail.com Date: Fri May 20 00:24:52 2011 +0200
2 Bugfixes
commit 9060108adccba661409ebd9cc7d6f59350cdc83f Author: tyron tyronx@gmail.com Date: Thu May 19 23:21:15 2011 +0200
This CSS Selectors breaks the style default link colors in jquery ui widgets, such as tabs
commit 58f6523c57cac23ba5d2e0d41e254834b8c8963a Author: tyron tyronx@gmail.com Date: Thu May 19 23:19:59 2011 +0200
- Server variable Filtering by category - Some fixes and style improvements - Tablesorting (sorting indicators are currently missing though)
commit 00515b13ba5cf90d387f7fc7588987084412b7ab Author: tyron tyronx@gmail.com Date: Thu May 19 23:15:53 2011 +0200
Added for cookie persistence on jquery ui tabs
commit d10e62ca37817c251cc3483b5d8ceea035759f9b Author: tyron tyronx@gmail.com Date: Wed May 18 14:25:15 2011 +0200
Added variable filtering for the 'Variables' Page
commit c0749d556378341727982e9ea08bb1de08e7c5ea Author: tyron tyronx@gmail.com Date: Wed May 18 13:17:47 2011 +0200
Organized tables into jQuery tabs to reduce odd scrolling effects when filtering variables
commit f8656fb0f8e9ecffa8abdc5dff1ab7c1b7e706db Author: tyron tyronx@gmail.com Date: Mon May 16 20:22:22 2011 +0200
Ajax loaded chart didn't remove zero values; filtered values have correct odd/even colored rows
commit 014a054c7f740aa6e172059ed440928685de6fd5 Merge: 6d7358abb34a229bc138ec9381dceb1eedd6d498 884ba99afccf9d67197a1d00e49e353baa5e00d0 Author: tyron tyronx@gmail.com Date: Mon May 16 15:48:03 2011 +0200
Merge remote-tracking branch 'origin/master'
commit 6d7358abb34a229bc138ec9381dceb1eedd6d498 Author: tyron tyronx@gmail.com Date: Mon May 16 15:41:59 2011 +0200
CSS changes for the single status variables table
commit 0839be45a5ac3281a3e0bb863ae804780bb87d9d Author: tyron tyronx@gmail.com Date: Mon May 16 15:34:57 2011 +0200
Status variables in one table, that can be filtered as-you-type with javascript also the query chart is loaded with ajax now. Both features degrade gracefully for non JS users.
commit 5ff65c4b54dfe8e0da176ca6f0cbb80cf3e0dfcf Merge: 9dc2cbd19cf68807a0994e80a4c1edba049f21e0 cd1b2817b6238491df1a84405ccda7c78499f21f Author: tyron tyronx@gmail.com Date: Fri May 13 14:59:31 2011 +0200
Merge remote-tracking branch 'origin/master'
commit 9dc2cbd19cf68807a0994e80a4c1edba049f21e0 Author: tyron tyronx@gmail.com Date: Thu May 5 10:15:27 2011 +0200
Merge branches 'tyronm' and 'master'
-----------------------------------------------------------------------
Summary of changes: chart_export.php | 34 + js/canvg/MIT-LICENSE.txt | 22 + js/canvg/canvg.js | 2221 +++++ js/canvg/rgbcolor.js | 288 + js/functions.js | 131 +- js/highcharts/exporting.js | 690 ++ js/highcharts/highcharts.js |10671 +++++++++++++++++++++++ js/jquery/jquery.cookie.js | 91 + js/jquery/jquery.tablesorter.js | 1031 +++ js/messages.php | 16 + js/pMap.js | 164 - js/server_status.js | 373 + js/server_variables.js | 42 + js/sql.js | 47 +- js/tbl_chart.js | 236 + libraries/chart.lib.php | 256 - libraries/chart/pChart/fonts/DejaVuSans.ttf | Bin 683528 -> 0 bytes libraries/chart/pChart/fonts/LICENSE | 99 - libraries/chart/pChart/fonts/README | 6 - libraries/chart/pChart/pCache.class | 119 - libraries/chart/pChart/pChart.class | 3626 -------- libraries/chart/pChart/pData.class | 260 - libraries/chart/pma_chart.php | 183 - libraries/chart/pma_pchart_chart.php | 402 - libraries/chart/pma_pchart_multi.php | 117 - libraries/chart/pma_pchart_multi_bar.php | 37 - libraries/chart/pma_pchart_multi_line.php | 38 - libraries/chart/pma_pchart_multi_radar.php | 107 - libraries/chart/pma_pchart_pie.php | 109 - libraries/chart/pma_pchart_single.php | 56 - libraries/chart/pma_pchart_single_bar.php | 34 - libraries/chart/pma_pchart_single_line.php | 34 - libraries/chart/pma_pchart_single_radar.php | 96 - libraries/chart/pma_pchart_stacked_bar.php | 35 - libraries/common.lib.php | 110 +- libraries/server_links.inc.php | 4 +- server_processlist.php | 114 - server_status.php | 1398 ++-- server_variables.php | 11 +- sql.php | 44 +- tbl_chart.php | 180 +- themes/original/css/theme_right.css.php | 67 +- themes/original/img/cleardot.gif | Bin 0 -> 43 bytes themes/original/jquery/jquery-ui-1.8.custom.css | 2 +- themes/pmahomme/css/theme_right.css.php | 193 +- themes/pmahomme/img/cleardot.gif | Bin 0 -> 43 bytes themes/pmahomme/img/s_sortable.png | Bin 0 -> 1107 bytes themes/pmahomme/jquery/jquery-ui-1.8.custom.css | 2 +- 48 files changed, 17048 insertions(+), 6748 deletions(-) create mode 100644 chart_export.php create mode 100644 js/canvg/MIT-LICENSE.txt create mode 100644 js/canvg/canvg.js create mode 100644 js/canvg/rgbcolor.js create mode 100644 js/highcharts/exporting.js create mode 100644 js/highcharts/highcharts.js create mode 100644 js/jquery/jquery.cookie.js create mode 100644 js/jquery/jquery.tablesorter.js delete mode 100644 js/pMap.js create mode 100644 js/server_status.js create mode 100644 js/server_variables.js create mode 100644 js/tbl_chart.js delete mode 100644 libraries/chart.lib.php delete mode 100644 libraries/chart/pChart/fonts/DejaVuSans.ttf delete mode 100644 libraries/chart/pChart/fonts/LICENSE delete mode 100644 libraries/chart/pChart/fonts/README delete mode 100644 libraries/chart/pChart/pCache.class delete mode 100644 libraries/chart/pChart/pChart.class delete mode 100644 libraries/chart/pChart/pData.class delete mode 100644 libraries/chart/pma_chart.php delete mode 100644 libraries/chart/pma_pchart_chart.php delete mode 100644 libraries/chart/pma_pchart_multi.php delete mode 100644 libraries/chart/pma_pchart_multi_bar.php delete mode 100644 libraries/chart/pma_pchart_multi_line.php delete mode 100644 libraries/chart/pma_pchart_multi_radar.php delete mode 100644 libraries/chart/pma_pchart_pie.php delete mode 100644 libraries/chart/pma_pchart_single.php delete mode 100644 libraries/chart/pma_pchart_single_bar.php delete mode 100644 libraries/chart/pma_pchart_single_line.php delete mode 100644 libraries/chart/pma_pchart_single_radar.php delete mode 100644 libraries/chart/pma_pchart_stacked_bar.php delete mode 100644 server_processlist.php create mode 100644 themes/original/img/cleardot.gif create mode 100644 themes/pmahomme/img/cleardot.gif create mode 100644 themes/pmahomme/img/s_sortable.png
diff --git a/chart_export.php b/chart_export.php new file mode 100644 index 0000000..87ab3a4 --- /dev/null +++ b/chart_export.php @@ -0,0 +1,34 @@ +<?php +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * "Echo" service to allow force downloading of exported charts (png or svg) + * + * @package phpMyAdmin + */ + + +define('PMA_MINIMUM_COMMON',true); + +require_once './libraries/common.inc.php'; + +if(isset($_REQUEST['filename']) && isset($_REQUEST['image'])) { + $allowed = Array( 'image/png'=>'png', 'image/svg+xml'=>'svg'); + + if(!isset($allowed[$_REQUEST['type']])) exit('Invalid export type'); + + if(!preg_match("/(".implode("|",$allowed).")$/i",$_REQUEST['filename'])) + $_REQUEST['filename'].='.'.$allowed[$_REQUEST['type']]; + + header("Cache-Control: public"); + header("Content-Description: File Transfer"); + header("Content-Disposition: attachment; filename=".$_REQUEST['filename']); + header("Content-Type: ".$_REQUEST['type']); + header("Content-Transfer-Encoding: binary"); + + if($allowed[$_REQUEST['type']]!='svg') + echo base64_decode(substr($_REQUEST['image'],strpos($_REQUEST['image'],',')+1)); + else + echo $_REQUEST['image']; + +} else exit('Invalid request'); +?> \ No newline at end of file diff --git a/js/canvg/MIT-LICENSE.txt b/js/canvg/MIT-LICENSE.txt new file mode 100644 index 0000000..40f19bd --- /dev/null +++ b/js/canvg/MIT-LICENSE.txt @@ -0,0 +1,22 @@ +Copyright (c) 2010-2011 Gabe Lerner (gabelerner@gmail.com) - http://code.google.com/p/canvg/ + + 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. \ No newline at end of file diff --git a/js/canvg/canvg.js b/js/canvg/canvg.js new file mode 100644 index 0000000..83c91b4 --- /dev/null +++ b/js/canvg/canvg.js @@ -0,0 +1,2221 @@ +/* + * canvg.js - Javascript SVG parser and renderer on Canvas + * MIT Licensed + * Gabe Lerner (gabelerner@gmail.com) + * http://code.google.com/p/canvg/ + * + * Requires: rgbcolor.js - http://www.phpied.com/rgb-color-parser-in-javascript/ + */ +if(!window.console) { + window.console = {}; + window.console.log = function(str) {}; + window.console.dir = function(str) {}; +} + +// <3 IE +if(!Array.indexOf){ + Array.prototype.indexOf = function(obj){ + for(var i=0; i<this.length; i++){ + if(this[i]==obj){ + return i; + } + } + return -1; + } +} + +(function(){ + // canvg(target, s) + // empty parameters: replace all 'svg' elements on page with 'canvas' elements + // target: canvas element or the id of a canvas element + // s: svg string or url to svg file + // opts: optional hash of options + // ignoreMouse: true => ignore mouse events + // ignoreAnimation: true => ignore animations + // ignoreDimensions: true => does not try to resize canvas + // ignoreClear: true => does not clear canvas + // offsetX: int => draws at a x offset + // offsetY: int => draws at a y offset + // scaleWidth: int => scales horizontally to width + // scaleHeight: int => scales vertically to height + // renderCallback: function => will call the function after the first render is completed + // forceRedraw: function => will call the function on every frame, if it returns true, will redraw + this.canvg = function (target, s, opts) { + // no parameters + if (target == null && s == null && opts == null) { + var svgTags = document.getElementsByTagName('svg'); + for (var i=0; i<svgTags.length; i++) { + var svgTag = svgTags[i]; + var c = document.createElement('canvas'); + c.width = svgTag.clientWidth; + c.height = svgTag.clientHeight; + svgTag.parentNode.insertBefore(c, svgTag); + svgTag.parentNode.removeChild(svgTag); + var div = document.createElement('div'); + div.appendChild(svgTag); + canvg(c, div.innerHTML); + } + return; + } + + if (typeof target == 'string') { + target = document.getElementById(target); + } + + // reuse class per canvas + var svg; + if (target.svg == null) { + svg = build(); + target.svg = svg; + } + else { + svg = target.svg; + svg.stop(); + } + svg.opts = opts; + + var ctx = target.getContext('2d'); + if (s.substr(0,1) == '<') { + // load from xml string + svg.loadXml(ctx, s); + } + else { + // load from url + svg.load(ctx, s); + } + } + + function build() { + var svg = { }; + + svg.FRAMERATE = 30; + + // globals + svg.init = function(ctx) { + svg.Definitions = {}; + svg.Styles = {}; + svg.Animations = []; + svg.Images = []; + svg.ctx = ctx; + svg.ViewPort = new (function () { + this.viewPorts = []; + this.SetCurrent = function(width, height) { this.viewPorts.push({ width: width, height: height }); } + this.RemoveCurrent = function() { this.viewPorts.pop(); } + this.Current = function() { return this.viewPorts[this.viewPorts.length - 1]; } + this.width = function() { return this.Current().width; } + this.height = function() { return this.Current().height; } + this.ComputeSize = function(d) { + if (d != null && typeof(d) == 'number') return d; + if (d == 'x') return this.width(); + if (d == 'y') return this.height(); + return Math.sqrt(Math.pow(this.width(), 2) + Math.pow(this.height(), 2)) / Math.sqrt(2); + } + }); + } + svg.init(); + + // images loaded + svg.ImagesLoaded = function() { + for (var i=0; i<svg.Images.length; i++) { + if (!svg.Images[i].loaded) return false; + } + return true; + } + + // trim + svg.trim = function(s) { return s.replace(/^\s+|\s+$/g, ''); } + + // compress spaces + svg.compressSpaces = function(s) { return s.replace(/[\s\r\t\n]+/gm,' '); } + + // ajax + svg.ajax = function(url) { + var AJAX; + if(window.XMLHttpRequest){AJAX=new XMLHttpRequest();} + else{AJAX=new ActiveXObject('Microsoft.XMLHTTP');} + if(AJAX){ + AJAX.open('GET',url,false); + AJAX.send(null); + return AJAX.responseText; + } + return null; + } + + // parse xml + svg.parseXml = function(xml) { + if (window.DOMParser) + { + var parser = new DOMParser(); + return parser.parseFromString(xml, 'text/xml'); + } + else + { + xml = xml.replace(/<!DOCTYPE svg[^>]*>/, ''); + var xmlDoc = new ActiveXObject('Microsoft.XMLDOM'); + xmlDoc.async = 'false'; + xmlDoc.loadXML(xml); + return xmlDoc; + } + } + + svg.Property = function(name, value) { + this.name = name; + this.value = value; + + this.hasValue = function() { + return (this.value != null && this.value != ''); + } + + // return the numerical value of the property + this.numValue = function() { + if (!this.hasValue()) return 0; + + var n = parseFloat(this.value); + if ((this.value + '').match(/%$/)) { + n = n / 100.0; + } + return n; + } + + this.valueOrDefault = function(def) { + if (this.hasValue()) return this.value; + return def; + } + + this.numValueOrDefault = function(def) { + if (this.hasValue()) return this.numValue(); + return def; + } + + /* EXTENSIONS */ + var that = this; + + // color extensions + this.Color = { + // augment the current color value with the opacity + addOpacity: function(opacity) { + var newValue = that.value; + if (opacity != null && opacity != '') { + var color = new RGBColor(that.value); + if (color.ok) { + newValue = 'rgba(' + color.r + ', ' + color.g + ', ' + color.b + ', ' + opacity + ')'; + } + } + return new svg.Property(that.name, newValue); + } + } + + // definition extensions + this.Definition = { + // get the definition from the definitions table + getDefinition: function() { + var name = that.value.replace(/^(url()?#([^)]+))?$/, '$2'); + return svg.Definitions[name]; + }, + + isUrl: function() { + return that.value.indexOf('url(') == 0 + }, + + getFillStyle: function(e) { + var def = this.getDefinition(); + + // gradient + if (def != null && def.createGradient) { + return def.createGradient(svg.ctx, e); + } + + // pattern + if (def != null && def.createPattern) { + return def.createPattern(svg.ctx, e); + } + + return null; + } + } + + // length extensions + this.Length = { + DPI: function(viewPort) { + return 96.0; // TODO: compute? + }, + + EM: function(viewPort) { + var em = 12; + + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + if (fontSize.hasValue()) em = fontSize.Length.toPixels(viewPort); + + return em; + }, + + // get the length as pixels + toPixels: function(viewPort) { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/em$/)) return that.numValue() * this.EM(viewPort); + if (s.match(/ex$/)) return that.numValue() * this.EM(viewPort) / 2.0; + if (s.match(/px$/)) return that.numValue(); + if (s.match(/pt$/)) return that.numValue() * 1.25; + if (s.match(/pc$/)) return that.numValue() * 15; + if (s.match(/cm$/)) return that.numValue() * this.DPI(viewPort) / 2.54; + if (s.match(/mm$/)) return that.numValue() * this.DPI(viewPort) / 25.4; + if (s.match(/in$/)) return that.numValue() * this.DPI(viewPort); + if (s.match(/%$/)) return that.numValue() * svg.ViewPort.ComputeSize(viewPort); + return that.numValue(); + } + } + + // time extensions + this.Time = { + // get the time as milliseconds + toMilliseconds: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/s$/)) return that.numValue() * 1000; + if (s.match(/ms$/)) return that.numValue(); + return that.numValue(); + } + } + + // angle extensions + this.Angle = { + // get the angle as radians + toRadians: function() { + if (!that.hasValue()) return 0; + var s = that.value+''; + if (s.match(/deg$/)) return that.numValue() * (Math.PI / 180.0); + if (s.match(/grad$/)) return that.numValue() * (Math.PI / 200.0); + if (s.match(/rad$/)) return that.numValue(); + return that.numValue() * (Math.PI / 180.0); + } + } + } + + // fonts + svg.Font = new (function() { + this.Styles = ['normal','italic','oblique','inherit']; + this.Variants = ['normal','small-caps','inherit']; + this.Weights = ['normal','bold','bolder','lighter','100','200','300','400','500','600','700','800','900','inherit']; + + this.CreateFont = function(fontStyle, fontVariant, fontWeight, fontSize, fontFamily, inherit) { + var f = inherit != null ? this.Parse(inherit) : this.CreateFont('', '', '', '', '', svg.ctx.font); + return { + fontFamily: fontFamily || f.fontFamily, + fontSize: fontSize || f.fontSize, + fontStyle: fontStyle || f.fontStyle, + fontWeight: fontWeight || f.fontWeight, + fontVariant: fontVariant || f.fontVariant, + toString: function () { return [this.fontStyle, this.fontVariant, this.fontWeight, this.fontSize, this.fontFamily].join(' ') } + } + } + + var that = this; + this.Parse = function(s) { + var f = {}; + var d = svg.trim(svg.compressSpaces(s || '')).split(' '); + var set = { fontSize: false, fontStyle: false, fontWeight: false, fontVariant: false } + var ff = ''; + for (var i=0; i<d.length; i++) { + if (!set.fontStyle && that.Styles.indexOf(d[i]) != -1) { if (d[i] != 'inherit') f.fontStyle = d[i]; set.fontStyle = true; } + else if (!set.fontVariant && that.Variants.indexOf(d[i]) != -1) { if (d[i] != 'inherit') f.fontVariant = d[i]; set.fontStyle = set.fontVariant = true; } + else if (!set.fontWeight && that.Weights.indexOf(d[i]) != -1) { if (d[i] != 'inherit') f.fontWeight = d[i]; set.fontStyle = set.fontVariant = set.fontWeight = true; } + else if (!set.fontSize) { if (d[i] != 'inherit') f.fontSize = d[i].split('/')[0]; set.fontStyle = set.fontVariant = set.fontWeight = set.fontSize = true; } + else { if (d[i] != 'inherit') ff += d[i]; } + } if (ff != '') f.fontFamily = ff; + return f; + } + }); + + // points and paths + svg.ToNumberArray = function(s) { + var a = svg.trim(svg.compressSpaces((s || '').replace(/,/g, ' '))).split(' '); + for (var i=0; i<a.length; i++) { + a[i] = parseFloat(a[i]); + } + return a; + } + svg.Point = function(x, y) { + this.x = x; + this.y = y; + + this.angleTo = function(p) { + return Math.atan2(p.y - this.y, p.x - this.x); + } + + this.applyTransform = function(v) { + var xp = this.x * v[0] + this.y * v[2] + v[4]; + var yp = this.x * v[1] + this.y * v[3] + v[5]; + this.x = xp; + this.y = yp; + } + } + svg.CreatePoint = function(s) { + var a = svg.ToNumberArray(s); + return new svg.Point(a[0], a[1]); + } + svg.CreatePath = function(s) { + var a = svg.ToNumberArray(s); + var path = []; + for (var i=0; i<a.length; i+=2) { + path.push(new svg.Point(a[i], a[i+1])); + } + return path; + } + + // bounding box + svg.BoundingBox = function(x1, y1, x2, y2) { // pass in initial points if you want + this.x1 = Number.NaN; + this.y1 = Number.NaN; + this.x2 = Number.NaN; + this.y2 = Number.NaN; + + this.x = function() { return this.x1; } + this.y = function() { return this.y1; } + this.width = function() { return this.x2 - this.x1; } + this.height = function() { return this.y2 - this.y1; } + + this.addPoint = function(x, y) { + if (x != null) { + if (isNaN(this.x1) || isNaN(this.x2)) { + this.x1 = x; + this.x2 = x; + } + if (x < this.x1) this.x1 = x; + if (x > this.x2) this.x2 = x; + } + + if (y != null) { + if (isNaN(this.y1) || isNaN(this.y2)) { + this.y1 = y; + this.y2 = y; + } + if (y < this.y1) this.y1 = y; + if (y > this.y2) this.y2 = y; + } + } + this.addX = function(x) { this.addPoint(x, null); } + this.addY = function(y) { this.addPoint(null, y); } + + this.addBoundingBox = function(bb) { + this.addPoint(bb.x1, bb.y1); + this.addPoint(bb.x2, bb.y2); + } + + this.addQuadraticCurve = function(p0x, p0y, p1x, p1y, p2x, p2y) { + var cp1x = p0x + 2/3 * (p1x - p0x); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp1y = p0y + 2/3 * (p1y - p0y); // CP1 = QP0 + 2/3 *(QP1-QP0) + var cp2x = cp1x + 1/3 * (p2x - p0x); // CP2 = CP1 + 1/3 *(QP2-QP0) + var cp2y = cp1y + 1/3 * (p2y - p0y); // CP2 = CP1 + 1/3 *(QP2-QP0) + this.addBezierCurve(p0x, p0y, cp1x, cp2x, cp1y, cp2y, p2x, p2y); + } + + this.addBezierCurve = function(p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y) { + // from http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding... + var p0 = [p0x, p0y], p1 = [p1x, p1y], p2 = [p2x, p2y], p3 = [p3x, p3y]; + this.addPoint(p0[0], p0[1]); + this.addPoint(p3[0], p3[1]); + + for (i=0; i<=1; i++) { + var f = function(t) { + return Math.pow(1-t, 3) * p0[i] + + 3 * Math.pow(1-t, 2) * t * p1[i] + + 3 * (1-t) * Math.pow(t, 2) * p2[i] + + Math.pow(t, 3) * p3[i]; + } + + var b = 6 * p0[i] - 12 * p1[i] + 6 * p2[i]; + var a = -3 * p0[i] + 9 * p1[i] - 9 * p2[i] + 3 * p3[i]; + var c = 3 * p1[i] - 3 * p0[i]; + + if (a == 0) { + if (b == 0) continue; + var t = -c / b; + if (0 < t && t < 1) { + if (i == 0) this.addX(f(t)); + if (i == 1) this.addY(f(t)); + } + continue; + } + + var b2ac = Math.pow(b, 2) - 4 * c * a; + if (b2ac < 0) continue; + var t1 = (-b + Math.sqrt(b2ac)) / (2 * a); + if (0 < t1 && t1 < 1) { + if (i == 0) this.addX(f(t1)); + if (i == 1) this.addY(f(t1)); + } + var t2 = (-b - Math.sqrt(b2ac)) / (2 * a); + if (0 < t2 && t2 < 1) { + if (i == 0) this.addX(f(t2)); + if (i == 1) this.addY(f(t2)); + } + } + } + + this.isPointInBox = function(x, y) { + return (this.x1 <= x && x <= this.x2 && this.y1 <= y && y <= this.y2); + } + + this.addPoint(x1, y1); + this.addPoint(x2, y2); + } + + // transforms + svg.Transform = function(v) { + var that = this; + this.Type = {} + + // translate + this.Type.translate = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.translate(this.p.x || 0.0, this.p.y || 0.0); + } + this.applyToPoint = function(p) { + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + } + } + + // rotate + this.Type.rotate = function(s) { + var a = svg.ToNumberArray(s); + this.angle = new svg.Property('angle', a[0]); + this.cx = a[1] || 0; + this.cy = a[2] || 0; + this.apply = function(ctx) { + ctx.translate(this.cx, this.cy); + ctx.rotate(this.angle.Angle.toRadians()); + ctx.translate(-this.cx, -this.cy); + } + this.applyToPoint = function(p) { + var a = this.angle.Angle.toRadians(); + p.applyTransform([1, 0, 0, 1, this.p.x || 0.0, this.p.y || 0.0]); + p.applyTransform([Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a), 0, 0]); + p.applyTransform([1, 0, 0, 1, -this.p.x || 0.0, -this.p.y || 0.0]); + } + } + + this.Type.scale = function(s) { + this.p = svg.CreatePoint(s); + this.apply = function(ctx) { + ctx.scale(this.p.x || 1.0, this.p.y || this.p.x || 1.0); + } + this.applyToPoint = function(p) { + p.applyTransform([this.p.x || 0.0, 0, 0, this.p.y || 0.0, 0, 0]); + } + } + + this.Type.matrix = function(s) { + this.m = svg.ToNumberArray(s); + this.apply = function(ctx) { + ctx.transform(this.m[0], this.m[1], this.m[2], this.m[3], this.m[4], this.m[5]); + } + this.applyToPoint = function(p) { + p.applyTransform(this.m); + } + } + + this.Type.SkewBase = function(s) { + this.base = that.Type.matrix; + this.base(s); + this.angle = new svg.Property('angle', s); + } + this.Type.SkewBase.prototype = new this.Type.matrix; + + this.Type.skewX = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, 0, Math.tan(this.angle.Angle.toRadians()), 1, 0, 0]; + } + this.Type.skewX.prototype = new this.Type.SkewBase; + + this.Type.skewY = function(s) { + this.base = that.Type.SkewBase; + this.base(s); + this.m = [1, Math.tan(this.angle.Angle.toRadians()), 0, 1, 0, 0]; + } + this.Type.skewY.prototype = new this.Type.SkewBase; + + this.transforms = []; + + this.apply = function(ctx) { + for (var i=0; i<this.transforms.length; i++) { + this.transforms[i].apply(ctx); + } + } + + this.applyToPoint = function(p) { + for (var i=0; i<this.transforms.length; i++) { + this.transforms[i].applyToPoint(p); + } + } + + var data = v.split(/\s(?=[a-z])/); + for (var i=0; i<data.length; i++) { + var type = data[i].split('(')[0]; + var s = data[i].split('(')[1].replace(')',''); + var transform = new this.Type[type](s); + this.transforms.push(transform); + } + } + + // aspect ratio + svg.AspectRatio = function(ctx, aspectRatio, width, desiredWidth, height, desiredHeight, minX, minY, refX, refY) { + // aspect ratio - http://www.w3.org/TR/SVG/coords.html#PreserveAspectRatioAttribute + aspectRatio = svg.compressSpaces(aspectRatio); + aspectRatio = aspectRatio.replace(/^defer\s/,''); // ignore defer + var align = aspectRatio.split(' ')[0] || 'xMidYMid'; + var meetOrSlice = aspectRatio.split(' ')[1] || 'meet'; + + // calculate scale + var scaleX = width / desiredWidth; + var scaleY = height / desiredHeight; + var scaleMin = Math.min(scaleX, scaleY); + var scaleMax = Math.max(scaleX, scaleY); + if (meetOrSlice == 'meet') { desiredWidth *= scaleMin; desiredHeight *= scaleMin; } + if (meetOrSlice == 'slice') { desiredWidth *= scaleMax; desiredHeight *= scaleMax; } + + refX = new svg.Property('refX', refX); + refY = new svg.Property('refY', refY); + if (refX.hasValue() && refY.hasValue()) { + ctx.translate(-scaleMin * refX.Length.toPixels('x'), -scaleMin * refY.Length.toPixels('y')); + } + else { + // align + if (align.match(/^xMid/) && ((meetOrSlice == 'meet' && scaleMin == scaleY) || (meetOrSlice == 'slice' && scaleMax == scaleY))) ctx.translate(width / 2.0 - desiredWidth / 2.0, 0); + if (align.match(/YMid$/) && ((meetOrSlice == 'meet' && scaleMin == scaleX) || (meetOrSlice == 'slice' && scaleMax == scaleX))) ctx.translate(0, height / 2.0 - desiredHeight / 2.0); + if (align.match(/^xMax/) && ((meetOrSlice == 'meet' && scaleMin == scaleY) || (meetOrSlice == 'slice' && scaleMax == scaleY))) ctx.translate(width - desiredWidth, 0); + if (align.match(/YMax$/) && ((meetOrSlice == 'meet' && scaleMin == scaleX) || (meetOrSlice == 'slice' && scaleMax == scaleX))) ctx.translate(0, height - desiredHeight); + } + + // scale + if (align == 'none') ctx.scale(scaleX, scaleY); + else if (meetOrSlice == 'meet') ctx.scale(scaleMin, scaleMin); + else if (meetOrSlice == 'slice') ctx.scale(scaleMax, scaleMax); + + // translate + ctx.translate(minX == null ? 0 : -minX, minY == null ? 0 : -minY); + } + + // elements + svg.Element = {} + + svg.Element.ElementBase = function(node) { + this.attributes = {}; + this.styles = {}; + this.children = []; + + // get or create attribute + this.attribute = function(name, createIfNotExists) { + var a = this.attributes[name]; + if (a != null) return a; + + a = new svg.Property(name, ''); + if (createIfNotExists == true) this.attributes[name] = a; + return a; + } + + // get or create style + this.style = function(name, createIfNotExists) { + var s = this.styles[name]; + if (s != null) return s; + + var a = this.attribute(name); + if (a != null && a.hasValue()) { + return a; + } + + s = new svg.Property(name, ''); + if (createIfNotExists == true) this.styles[name] = s; + return s; + } + + // base render + this.render = function(ctx) { + // don't render display=none + if (this.attribute('display').value == 'none') return; + + ctx.save(); + this.setContext(ctx); + this.renderChildren(ctx); + this.clearContext(ctx); + ctx.restore(); + } + + // base set context + this.setContext = function(ctx) { + // OVERRIDE ME! + } + + // base clear context + this.clearContext = function(ctx) { + // OVERRIDE ME! + } + + // base render children + this.renderChildren = function(ctx) { + for (var i=0; i<this.children.length; i++) { + this.children[i].render(ctx); + } + } + + this.addChild = function(childNode, create) { + var child = childNode; + if (create) child = svg.CreateElement(childNode); + child.parent = this; + this.children.push(child); + } + + if (node != null && node.nodeType == 1) { //ELEMENT_NODE + // add children + for (var i=0; i<node.childNodes.length; i++) { + var childNode = node.childNodes[i]; + if (childNode.nodeType == 1) this.addChild(childNode, true); //ELEMENT_NODE + } + + // add attributes + for (var i=0; i<node.attributes.length; i++) { + var attribute = node.attributes[i]; + this.attributes[attribute.nodeName] = new svg.Property(attribute.nodeName, attribute.nodeValue); + } + + // add tag styles + var styles = svg.Styles[this.type]; + if (styles != null) { + for (var name in styles) { + this.styles[name] = styles[name]; + } + } + + // add class styles + if (this.attribute('class').hasValue()) { + var classes = svg.compressSpaces(this.attribute('class').value).split(' '); + for (var j=0; j<classes.length; j++) { + styles = svg.Styles['.'+classes[j]]; + if (styles != null) { + for (var name in styles) { + this.styles[name] = styles[name]; + } + } + } + } + + // add inline styles + if (this.attribute('style').hasValue()) { + var styles = this.attribute('style').value.split(';'); + for (var i=0; i<styles.length; i++) { + if (svg.trim(styles[i]) != '') { + var style = styles[i].split(':'); + var name = svg.trim(style[0]); + var value = svg.trim(style[1]); + this.styles[name] = new svg.Property(name, value); + } + } + } + + // add id + if (this.attribute('id').hasValue()) { + if (svg.Definitions[this.attribute('id').value] == null) { + svg.Definitions[this.attribute('id').value] = this; + } + } + } + } + + svg.Element.RenderedElementBase = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.setContext = function(ctx) { + // fill + if (this.style('fill').Definition.isUrl()) { + var fs = this.style('fill').Definition.getFillStyle(this); + if (fs != null) ctx.fillStyle = fs; + } + else if (this.style('fill').hasValue()) { + var fillStyle = this.style('fill'); + if (this.style('fill-opacity').hasValue()) fillStyle = fillStyle.Color.addOpacity(this.style('fill-opacity').value); + ctx.fillStyle = (fillStyle.value == 'none' ? 'rgba(0,0,0,0)' : fillStyle.value); + } + + // stroke + if (this.style('stroke').Definition.isUrl()) { + var fs = this.style('stroke').Definition.getFillStyle(this); + if (fs != null) ctx.strokeStyle = fs; + } + else if (this.style('stroke').hasValue()) { + var strokeStyle = this.style('stroke'); + if (this.style('stroke-opacity').hasValue()) strokeStyle = strokeStyle.Color.addOpacity(this.style('stroke-opacity').value); + ctx.strokeStyle = (strokeStyle.value == 'none' ? 'rgba(0,0,0,0)' : strokeStyle.value); + } + if (this.style('stroke-width').hasValue()) ctx.lineWidth = this.style('stroke-width').Length.toPixels(); + if (this.style('stroke-linecap').hasValue()) ctx.lineCap = this.style('stroke-linecap').value; + if (this.style('stroke-linejoin').hasValue()) ctx.lineJoin = this.style('stroke-linejoin').value; + if (this.style('stroke-miterlimit').hasValue()) ctx.miterLimit = this.style('stroke-miterlimit').value; + + // font + if (typeof(ctx.font) != 'undefined') { + ctx.font = svg.Font.CreateFont( + this.style('font-style').value, + this.style('font-variant').value, + this.style('font-weight').value, + this.style('font-size').hasValue() ? this.style('font-size').Length.toPixels() + 'px' : '', + this.style('font-family').value).toString(); + } + + // transform + if (this.attribute('transform').hasValue()) { + var transform = new svg.Transform(this.attribute('transform').value); + transform.apply(ctx); + } + + // clip + if (this.attribute('clip-path').hasValue()) { + var clip = this.attribute('clip-path').Definition.getDefinition(); + if (clip != null) clip.apply(ctx); + } + + // opacity + if (this.style('opacity').hasValue()) { + ctx.globalAlpha = this.style('opacity').numValue(); + } + } + } + svg.Element.RenderedElementBase.prototype = new svg.Element.ElementBase; + + svg.Element.PathElementBase = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.path = function(ctx) { + if (ctx != null) ctx.beginPath(); + return new svg.BoundingBox(); + } + + this.renderChildren = function(ctx) { + this.path(ctx); + svg.Mouse.checkPath(this, ctx); + if (ctx.fillStyle != '') ctx.fill(); + if (ctx.strokeStyle != '') ctx.stroke(); + + var markers = this.getMarkers(); + if (markers != null) { + if (this.attribute('marker-start').Definition.isUrl()) { + var marker = this.attribute('marker-start').Definition.getDefinition(); + marker.render(ctx, markers[0][0], markers[0][1]); + } + if (this.attribute('marker-mid').Definition.isUrl()) { + var marker = this.attribute('marker-mid').Definition.getDefinition(); + for (var i=1;i<markers.length-1;i++) { + marker.render(ctx, markers[i][0], markers[i][1]); + } + } + if (this.attribute('marker-end').Definition.isUrl()) { + var marker = this.attribute('marker-end').Definition.getDefinition(); + marker.render(ctx, markers[markers.length-1][0], markers[markers.length-1][1]); + } + } + } + + this.getBoundingBox = function() { + return this.path(); + } + + this.getMarkers = function() { + return null; + } + } + svg.Element.PathElementBase.prototype = new svg.Element.RenderedElementBase; + + // svg element + svg.Element.svg = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.baseClearContext = this.clearContext; + this.clearContext = function(ctx) { + this.baseClearContext(ctx); + svg.ViewPort.RemoveCurrent(); + } + + this.baseSetContext = this.setContext; + this.setContext = function(ctx) { + // initial values + ctx.strokeStyle = 'rgba(0,0,0,0)'; + ctx.lineCap = 'butt'; + ctx.lineJoin = 'miter'; + ctx.miterLimit = 4; + + this.baseSetContext(ctx); + + // create new view port + if (this.attribute('x').hasValue() && this.attribute('y').hasValue()) { + ctx.translate(this.attribute('x').Length.toPixels('x'), this.attribute('y').Length.toPixels('y')); + } + + var width = svg.ViewPort.width(); + var height = svg.ViewPort.height(); + if (this.attribute('width').hasValue() && this.attribute('height').hasValue()) { + width = this.attribute('width').Length.toPixels('x'); + height = this.attribute('height').Length.toPixels('y'); + + var x = 0; + var y = 0; + if (this.attribute('refX').hasValue() && this.attribute('refY').hasValue()) { + x = -this.attribute('refX').Length.toPixels('x'); + y = -this.attribute('refY').Length.toPixels('y'); + } + + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(width, y); + ctx.lineTo(width, height); + ctx.lineTo(x, height); + ctx.closePath(); + ctx.clip(); + } + svg.ViewPort.SetCurrent(width, height); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + var viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + var minX = viewBox[0]; + var minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio(ctx, + this.attribute('preserveAspectRatio').value, + svg.ViewPort.width(), + width, + svg.ViewPort.height(), + height, + minX, + minY, + this.attribute('refX').value, + this.attribute('refY').value); + + svg.ViewPort.RemoveCurrent(); + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + } + } + svg.Element.svg.prototype = new svg.Element.RenderedElementBase; + + // rect element + svg.Element.rect = function(node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.path = function(ctx) { + var x = this.attribute('x').Length.toPixels('x'); + var y = this.attribute('y').Length.toPixels('y'); + var width = this.attribute('width').Length.toPixels('x'); + var height = this.attribute('height').Length.toPixels('y'); + var rx = this.attribute('rx').Length.toPixels('x'); + var ry = this.attribute('ry').Length.toPixels('y'); + if (this.attribute('rx').hasValue() && !this.attribute('ry').hasValue()) ry = rx; + if (this.attribute('ry').hasValue() && !this.attribute('rx').hasValue()) rx = ry; + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(x + rx, y); + ctx.lineTo(x + width - rx, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + ry) + ctx.lineTo(x + width, y + height - ry); + ctx.quadraticCurveTo(x + width, y + height, x + width - rx, y + height) + ctx.lineTo(x + rx, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - ry) + ctx.lineTo(x, y + ry); + ctx.quadraticCurveTo(x, y, x + rx, y) + ctx.closePath(); + } + + return new svg.BoundingBox(x, y, x + width, y + height); + } + } + svg.Element.rect.prototype = new svg.Element.PathElementBase; + + // circle element + svg.Element.circle = function(node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.path = function(ctx) { + var cx = this.attribute('cx').Length.toPixels('x'); + var cy = this.attribute('cy').Length.toPixels('y'); + var r = this.attribute('r').Length.toPixels(); + + if (ctx != null) { + ctx.beginPath(); + ctx.arc(cx, cy, r, 0, Math.PI * 2, true); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - r, cy - r, cx + r, cy + r); + } + } + svg.Element.circle.prototype = new svg.Element.PathElementBase; + + // ellipse element + svg.Element.ellipse = function(node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.path = function(ctx) { + var KAPPA = 4 * ((Math.sqrt(2) - 1) / 3); + var rx = this.attribute('rx').Length.toPixels('x'); + var ry = this.attribute('ry').Length.toPixels('y'); + var cx = this.attribute('cx').Length.toPixels('x'); + var cy = this.attribute('cy').Length.toPixels('y'); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(cx, cy - ry); + ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy); + ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry); + ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy); + ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry); + ctx.closePath(); + } + + return new svg.BoundingBox(cx - rx, cy - ry, cx + rx, cy + ry); + } + } + svg.Element.ellipse.prototype = new svg.Element.PathElementBase; + + // line element + svg.Element.line = function(node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.getPoints = function() { + return [ + new svg.Point(this.attribute('x1').Length.toPixels('x'), this.attribute('y1').Length.toPixels('y')), + new svg.Point(this.attribute('x2').Length.toPixels('x'), this.attribute('y2').Length.toPixels('y'))]; + } + + this.path = function(ctx) { + var points = this.getPoints(); + + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + ctx.lineTo(points[1].x, points[1].y); + } + + return new svg.BoundingBox(points[0].x, points[0].y, points[1].x, points[1].y); + } + + this.getMarkers = function() { + var points = this.getPoints(); + var a = points[0].angleTo(points[1]); + return [[points[0], a], [points[1], a]]; + } + } + svg.Element.line.prototype = new svg.Element.PathElementBase; + + // polyline element + svg.Element.polyline = function(node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + this.points = svg.CreatePath(this.attribute('points').value); + this.path = function(ctx) { + var bb = new svg.BoundingBox(this.points[0].x, this.points[0].y); + if (ctx != null) { + ctx.beginPath(); + ctx.moveTo(this.points[0].x, this.points[0].y); + } + for (var i=1; i<this.points.length; i++) { + bb.addPoint(this.points[i].x, this.points[i].y); + if (ctx != null) ctx.lineTo(this.points[i].x, this.points[i].y); + } + return bb; + } + + this.getMarkers = function() { + var markers = []; + for (var i=0; i<this.points.length - 1; i++) { + markers.push([this.points[i], this.points[i].angleTo(this.points[i+1])]); + } + markers.push([this.points[this.points.length-1], markers[markers.length-1][1]]); + return markers; + } + } + svg.Element.polyline.prototype = new svg.Element.PathElementBase; + + // polygon element + svg.Element.polygon = function(node) { + this.base = svg.Element.polyline; + this.base(node); + + this.basePath = this.path; + this.path = function(ctx) { + var bb = this.basePath(ctx); + if (ctx != null) { + ctx.lineTo(this.points[0].x, this.points[0].y); + ctx.closePath(); + } + return bb; + } + } + svg.Element.polygon.prototype = new svg.Element.polyline; + + // path element + svg.Element.path = function(node) { + this.base = svg.Element.PathElementBase; + this.base(node); + + var d = this.attribute('d').value; + // TODO: floating points, convert to real lexer based on http://www.w3.org/TR/SVG11/paths.html#PathDataBNF + d = d.replace(/,/gm,' '); // get rid of all commas + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm,'$1 $2'); // separate commands from commands + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([MmZzLlHhVvCcSsQqTtAa])/gm,'$1 $2'); // separate commands from commands + d = d.replace(/([MmZzLlHhVvCcSsQqTtAa])([^\s])/gm,'$1 $2'); // separate commands from points + d = d.replace(/([^\s])([MmZzLlHhVvCcSsQqTtAa])/gm,'$1 $2'); // separate commands from points + d = d.replace(/([0-9])([+-])/gm,'$1 $2'); // separate digits when no comma + d = d.replace(/(.[0-9]*)(.)/gm,'$1 $2'); // separate digits when no comma + d = d.replace(/([Aa](\s+[0-9]+){3})\s+([01])\s*([01])/gm,'$1 $3 $4 '); // shorthand elliptical arc path syntax + d = svg.compressSpaces(d); // compress multiple spaces + d = svg.trim(d); + this.PathParser = new (function(d) { + this.tokens = d.split(' '); + + this.reset = function() { + this.i = -1; + this.command = ''; + this.previousCommand = ''; + this.start = new svg.Point(0, 0); + this.control = new svg.Point(0, 0); + this.current = new svg.Point(0, 0); + this.points = []; + this.angles = []; + } + + this.isEnd = function() { + return this.i >= this.tokens.length - 1; + } + + this.isCommandOrEnd = function() { + if (this.isEnd()) return true; + return this.tokens[this.i + 1].match(/[A-Za-z]/) != null; + } + + this.isRelativeCommand = function() { + return this.command == this.command.toLowerCase(); + } + + this.getToken = function() { + this.i = this.i + 1; + return this.tokens[this.i]; + } + + this.getScalar = function() { + return parseFloat(this.getToken()); + } + + this.nextCommand = function() { + this.previousCommand = this.command; + this.command = this.getToken(); + } + + this.getPoint = function() { + var p = new svg.Point(this.getScalar(), this.getScalar()); + return this.makeAbsolute(p); + } + + this.getAsControlPoint = function() { + var p = this.getPoint(); + this.control = p; + return p; + } + + this.getAsCurrentPoint = function() { + var p = this.getPoint(); + this.current = p; + return p; + } + + this.getReflectedControlPoint = function() { + if (this.previousCommand.toLowerCase() != 'c' && this.previousCommand.toLowerCase() != 's') { + return this.current; + } + + // reflect point + var p = new svg.Point(2 * this.current.x - this.control.x, 2 * this.current.y - this.control.y); + return p; + } + + this.makeAbsolute = function(p) { + if (this.isRelativeCommand()) { + p.x = this.current.x + p.x; + p.y = this.current.y + p.y; + } + return p; + } + + this.addMarker = function(p, from) { + this.addMarkerAngle(p, from == null ? null : from.angleTo(p)); + } + + this.addMarkerAngle = function(p, a) { + this.points.push(p); + this.angles.push(a); + } + + this.getMarkerPoints = function() { return this.points; } + this.getMarkerAngles = function() { + for (var i=0; i<this.angles.length; i++) { + if (this.angles[i] == null) { + for (var j=i+1; j<this.angles.length; j++) { + if (this.angles[j] != null) { + this.angles[i] = this.angles[j]; + break; + } + } + } + } + return this.angles; + } + })(d); + + this.path = function(ctx) { + var pp = this.PathParser; + pp.reset(); + + var bb = new svg.BoundingBox(); + + if(this.attribute('visibility').value=='hidden') return; + + if (ctx != null) ctx.beginPath(); + while (!pp.isEnd()) { + pp.nextCommand(); + switch (pp.command.toUpperCase()) { + case 'M': + var p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.moveTo(p.x, p.y); + pp.start = pp.current; + while (!pp.isCommandOrEnd()) { + var p = pp.getAsCurrentPoint(); + pp.addMarker(p); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'L': + while (!pp.isCommandOrEnd()) { + var c = pp.current; + var p = pp.getAsCurrentPoint(); + pp.addMarker(p, c); + bb.addPoint(p.x, p.y); + if (ctx != null) ctx.lineTo(p.x, p.y); + } + break; + case 'H': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point((pp.isRelativeCommand() ? pp.current.x : 0) + pp.getScalar(), pp.current.y); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'V': + while (!pp.isCommandOrEnd()) { + var newP = new svg.Point(pp.current.x, (pp.isRelativeCommand() ? pp.current.y : 0) + pp.getScalar()); + pp.addMarker(newP, pp.current); + pp.current = newP; + bb.addPoint(pp.current.x, pp.current.y); + if (ctx != null) ctx.lineTo(pp.current.x, pp.current.y); + } + break; + case 'C': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var p1 = pp.getPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'S': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var p1 = pp.getReflectedControlPoint(); + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl); + bb.addBezierCurve(curr.x, curr.y, p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.bezierCurveTo(p1.x, p1.y, cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'Q': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var cntrl = pp.getAsControlPoint(); + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'T': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var cntrl = pp.getReflectedControlPoint(); + pp.control = cntrl; + var cp = pp.getAsCurrentPoint(); + pp.addMarker(cp, cntrl); + bb.addQuadraticCurve(curr.x, curr.y, cntrl.x, cntrl.y, cp.x, cp.y); + if (ctx != null) ctx.quadraticCurveTo(cntrl.x, cntrl.y, cp.x, cp.y); + } + break; + case 'A': + while (!pp.isCommandOrEnd()) { + var curr = pp.current; + var rx = pp.getScalar(); + var ry = pp.getScalar(); + var xAxisRotation = pp.getScalar() * (Math.PI / 180.0); + var largeArcFlag = pp.getScalar(); + var sweepFlag = pp.getScalar(); + var cp = pp.getAsCurrentPoint(); + + // Conversion from endpoint to center parameterization + // http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes + // x1', y1' + var currp = new svg.Point( + Math.cos(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.sin(xAxisRotation) * (curr.y - cp.y) / 2.0, + -Math.sin(xAxisRotation) * (curr.x - cp.x) / 2.0 + Math.cos(xAxisRotation) * (curr.y - cp.y) / 2.0 + ); + // adjust radii + var l = Math.pow(currp.x,2)/Math.pow(rx,2)+Math.pow(currp.y,2)/Math.pow(ry,2); + if (l > 1) { + rx *= Math.sqrt(l); + ry *= Math.sqrt(l); + } + // cx', cy' + var s = (largeArcFlag == sweepFlag ? -1 : 1) * Math.sqrt( + ((Math.pow(rx,2)*Math.pow(ry,2))-(Math.pow(rx,2)*Math.pow(currp.y,2))-(Math.pow(ry,2)*Math.pow(currp.x,2))) / + (Math.pow(rx,2)*Math.pow(currp.y,2)+Math.pow(ry,2)*Math.pow(currp.x,2)) + ); + if (isNaN(s)) s = 0; + var cpp = new svg.Point(s * rx * currp.y / ry, s * -ry * currp.x / rx); + // cx, cy + var centp = new svg.Point( + (curr.x + cp.x) / 2.0 + Math.cos(xAxisRotation) * cpp.x - Math.sin(xAxisRotation) * cpp.y, + (curr.y + cp.y) / 2.0 + Math.sin(xAxisRotation) * cpp.x + Math.cos(xAxisRotation) * cpp.y + ); + // vector magnitude + var m = function(v) { return Math.sqrt(Math.pow(v[0],2) + Math.pow(v[1],2)); } + // ratio between two vectors + var r = function(u, v) { return (u[0]*v[0]+u[1]*v[1]) / (m(u)*m(v)) } + // angle between two vectors + var a = function(u, v) { return (u[0]*v[1] < u[1]*v[0] ? -1 : 1) * Math.acos(r(u,v)); } + // initial angle + var a1 = a([1,0], [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]); + // angle delta + var u = [(currp.x-cpp.x)/rx,(currp.y-cpp.y)/ry]; + var v = [(-currp.x-cpp.x)/rx,(-currp.y-cpp.y)/ry]; + var ad = a(u, v); + if (r(u,v) <= -1) ad = Math.PI; + if (r(u,v) >= 1) ad = 0; + + if (sweepFlag == 0 && ad > 0) ad = ad - 2 * Math.PI; + if (sweepFlag == 1 && ad < 0) ad = ad + 2 * Math.PI; + + // for markers + var halfWay = new svg.Point( + centp.x - rx * Math.cos((a1 + ad) / 2), + centp.y - ry * Math.sin((a1 + ad) / 2) + ); + pp.addMarkerAngle(halfWay, (a1 + ad) / 2 + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); + pp.addMarkerAngle(cp, ad + (sweepFlag == 0 ? 1 : -1) * Math.PI / 2); + + bb.addPoint(cp.x, cp.y); // TODO: this is too naive, make it better + if (ctx != null) { + var r = rx > ry ? rx : ry; + var sx = rx > ry ? 1 : rx / ry; + var sy = rx > ry ? ry / rx : 1; + + ctx.translate(centp.x, centp.y); + ctx.rotate(xAxisRotation); + ctx.scale(sx, sy); + ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepFlag); + ctx.scale(1/sx, 1/sy); + ctx.rotate(-xAxisRotation); + ctx.translate(-centp.x, -centp.y); + } + } + break; + case 'Z': + if (ctx != null) ctx.closePath(); + pp.current = pp.start; + } + } + + return bb; + } + + this.getMarkers = function() { + var points = this.PathParser.getMarkerPoints(); + var angles = this.PathParser.getMarkerAngles(); + + var markers = []; + for (var i=0; i<points.length; i++) { + markers.push([points[i], angles[i]]); + } + return markers; + } + } + svg.Element.path.prototype = new svg.Element.PathElementBase; + + // pattern element + svg.Element.pattern = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.createPattern = function(ctx, element) { + // render me using a temporary svg element + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['x'] = new svg.Property('x', this.attribute('x').value); + tempSvg.attributes['y'] = new svg.Property('y', this.attribute('y').value); + tempSvg.attributes['width'] = new svg.Property('width', this.attribute('width').value); + tempSvg.attributes['height'] = new svg.Property('height', this.attribute('height').value); + tempSvg.children = this.children; + + var c = document.createElement('canvas'); + c.width = this.attribute('width').Length.toPixels(); + c.height = this.attribute('height').Length.toPixels(); + tempSvg.render(c.getContext('2d')); + return ctx.createPattern(c, 'repeat'); + } + } + svg.Element.pattern.prototype = new svg.Element.ElementBase; + + // marker element + svg.Element.marker = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.baseRender = this.render; + this.render = function(ctx, point, angle) { + ctx.translate(point.x, point.y); + if (this.attribute('orient').valueOrDefault('auto') == 'auto') ctx.rotate(angle); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') == 'strokeWidth') ctx.scale(ctx.lineWidth, ctx.lineWidth); + ctx.save(); + + // render me using a temporary svg element + var tempSvg = new svg.Element.svg(); + tempSvg.attributes['viewBox'] = new svg.Property('viewBox', this.attribute('viewBox').value); + tempSvg.attributes['refX'] = new svg.Property('refX', this.attribute('refX').value); + tempSvg.attributes['refY'] = new svg.Property('refY', this.attribute('refY').value); + tempSvg.attributes['width'] = new svg.Property('width', this.attribute('markerWidth').value); + tempSvg.attributes['height'] = new svg.Property('height', this.attribute('markerHeight').value); + tempSvg.attributes['fill'] = new svg.Property('fill', this.attribute('fill').valueOrDefault('black')); + tempSvg.attributes['stroke'] = new svg.Property('stroke', this.attribute('stroke').valueOrDefault('none')); + tempSvg.children = this.children; + tempSvg.render(ctx); + + ctx.restore(); + if (this.attribute('markerUnits').valueOrDefault('strokeWidth') == 'strokeWidth') ctx.scale(1/ctx.lineWidth, 1/ctx.lineWidth); + if (this.attribute('orient').valueOrDefault('auto') == 'auto') ctx.rotate(-angle); + ctx.translate(-point.x, -point.y); + } + } + svg.Element.marker.prototype = new svg.Element.ElementBase; + + // definitions element + svg.Element.defs = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.render = function(ctx) { + // NOOP + } + } + svg.Element.defs.prototype = new svg.Element.ElementBase; + + // base for gradients + svg.Element.GradientBase = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.gradientUnits = this.attribute('gradientUnits').valueOrDefault('objectBoundingBox'); + + this.stops = []; + for (var i=0; i<this.children.length; i++) { + var child = this.children[i]; + this.stops.push(child); + } + + this.getGradient = function() { + // OVERRIDE ME! + } + + this.createGradient = function(ctx, element) { + var stopsContainer = this; + if (this.attribute('xlink:href').hasValue()) { + stopsContainer = this.attribute('xlink:href').Definition.getDefinition(); + } + + var g = this.getGradient(ctx, element); + for (var i=0; i<stopsContainer.stops.length; i++) { + g.addColorStop(stopsContainer.stops[i].offset, stopsContainer.stops[i].color); + } + return g; + } + } + svg.Element.GradientBase.prototype = new svg.Element.ElementBase; + + // linear gradient element + svg.Element.linearGradient = function(node) { + this.base = svg.Element.GradientBase; + this.base(node); + + this.getGradient = function(ctx, element) { + var bb = element.getBoundingBox(); + + var x1 = (this.gradientUnits == 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('x1').numValue() + : this.attribute('x1').Length.toPixels('x')); + var y1 = (this.gradientUnits == 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('y1').numValue() + : this.attribute('y1').Length.toPixels('y')); + var x2 = (this.gradientUnits == 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('x2').numValue() + : this.attribute('x2').Length.toPixels('x')); + var y2 = (this.gradientUnits == 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('y2').numValue() + : this.attribute('y2').Length.toPixels('y')); + + var p1 = new svg.Point(x1, y1); + var p2 = new svg.Point(x2, y2); + if (this.attribute('gradientTransform').hasValue()) { + var transform = new svg.Transform(this.attribute('gradientTransform').value); + transform.applyToPoint(p1); + transform.applyToPoint(p2); + } + + return ctx.createLinearGradient(p1.x, p1.y, p2.x, p2.y); + } + } + svg.Element.linearGradient.prototype = new svg.Element.GradientBase; + + // radial gradient element + svg.Element.radialGradient = function(node) { + this.base = svg.Element.GradientBase; + this.base(node); + + this.getGradient = function(ctx, element) { + var bb = element.getBoundingBox(); + + var cx = (this.gradientUnits == 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('cx').numValue() + : this.attribute('cx').Length.toPixels('x')); + var cy = (this.gradientUnits == 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('cy').numValue() + : this.attribute('cy').Length.toPixels('y')); + + var fx = cx; + var fy = cy; + if (this.attribute('fx').hasValue()) { + fx = (this.gradientUnits == 'objectBoundingBox' + ? bb.x() + bb.width() * this.attribute('fx').numValue() + : this.attribute('fx').Length.toPixels('x')); + } + if (this.attribute('fy').hasValue()) { + fy = (this.gradientUnits == 'objectBoundingBox' + ? bb.y() + bb.height() * this.attribute('fy').numValue() + : this.attribute('fy').Length.toPixels('y')); + } + + var r = (this.gradientUnits == 'objectBoundingBox' + ? (bb.width() + bb.height()) / 2.0 * this.attribute('r').numValue() + : this.attribute('r').Length.toPixels()); + + var c = new svg.Point(cx, cy); + var f = new svg.Point(fx, fy); + if (this.attribute('gradientTransform').hasValue()) { + var transform = new svg.Transform(this.attribute('gradientTransform').value); + transform.applyToPoint(c); + transform.applyToPoint(f); + + for (var i=0; i<transform.transforms.length; i++) { + // average the scaling part of the transform, apply to radius + var scale1 = transform.transforms[i].m[0]; + var scale2 = transform.transforms[i].m[3]; + r = r * ((scale1 + scale2) / 2.0); + } + } + + return ctx.createRadialGradient(f.x, f.y, 0, c.x, c.y, r); + } + } + svg.Element.radialGradient.prototype = new svg.Element.GradientBase; + + // gradient stop element + svg.Element.stop = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.offset = this.attribute('offset').numValue(); + + var stopColor = this.style('stop-color'); + if (this.style('stop-opacity').hasValue()) stopColor = stopColor.Color.addOpacity(this.style('stop-opacity').value); + this.color = stopColor.value; + } + svg.Element.stop.prototype = new svg.Element.ElementBase; + + // animation base element + svg.Element.AnimateBase = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + svg.Animations.push(this); + + this.duration = 0.0; + this.begin = this.attribute('begin').Time.toMilliseconds(); + this.maxDuration = this.begin + this.attribute('dur').Time.toMilliseconds(); + + this.getProperty = function() { + var attributeType = this.attribute('attributeType').value; + var attributeName = this.attribute('attributeName').value; + + if (attributeType == 'CSS') { + return this.parent.style(attributeName, true); + } + return this.parent.attribute(attributeName, true); + }; + + this.initialValue = null; + this.removed = false; + + this.calcValue = function() { + // OVERRIDE ME! + return ''; + } + + this.update = function(delta) { + // set initial value + if (this.initialValue == null) { + this.initialValue = this.getProperty().value; + } + + // if we're past the end time + if (this.duration > this.maxDuration) { + // loop for indefinitely repeating animations + if (this.attribute('repeatCount').value == 'indefinite') { + this.duration = 0.0 + } + else if (this.attribute('fill').valueOrDefault('remove') == 'remove' && !this.removed) { + this.removed = true; + this.getProperty().value = this.initialValue; + return true; + } + else { + return false; // no updates made + } + } + this.duration = this.duration + delta; + + // if we're past the begin time + var updated = false; + if (this.begin < this.duration) { + var newValue = this.calcValue(); // tween + + if (this.attribute('type').hasValue()) { + // for transform, etc. + var type = this.attribute('type').value; + newValue = type + '(' + newValue + ')'; + } + + this.getProperty().value = newValue; + updated = true; + } + + return updated; + } + + // fraction of duration we've covered + this.progress = function() { + return ((this.duration - this.begin) / (this.maxDuration - this.begin)); + } + } + svg.Element.AnimateBase.prototype = new svg.Element.ElementBase; + + // animate element + svg.Element.animate = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = this.attribute('from').numValue(); + var to = this.attribute('to').numValue(); + + // tween value linearly + return from + (to - from) * this.progress(); + }; + } + svg.Element.animate.prototype = new svg.Element.AnimateBase; + + // animate color element + svg.Element.animateColor = function(node) { + this.base = svg.Element.AnimateBase; + this.base(node); + + this.calcValue = function() { + var from = new RGBColor(this.attribute('from').value); + var to = new RGBColor(this.attribute('to').value); + + if (from.ok && to.ok) { + // tween color linearly + var r = from.r + (to.r - from.r) * this.progress(); + var g = from.g + (to.g - from.g) * this.progress(); + var b = from.b + (to.b - from.b) * this.progress(); + return 'rgb('+parseInt(r,10)+','+parseInt(g,10)+','+parseInt(b,10)+')'; + } + return this.attribute('from').value; + }; + } + svg.Element.animateColor.prototype = new svg.Element.AnimateBase; + + // animate transform element + svg.Element.animateTransform = function(node) { + this.base = svg.Element.animate; + this.base(node); + } + svg.Element.animateTransform.prototype = new svg.Element.animate; + + // text element + svg.Element.text = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + if (node != null) { + // add children + this.children = []; + for (var i=0; i<node.childNodes.length; i++) { + var childNode = node.childNodes[i]; + if (childNode.nodeType == 1) { // capture tspan and tref nodes + this.addChild(childNode, true); + } + else if (childNode.nodeType == 3) { // capture text + this.addChild(new svg.Element.tspan(childNode), false); + } + } + } + + this.baseSetContext = this.setContext; + this.setContext = function(ctx) { + this.baseSetContext(ctx); + if (this.attribute('text-anchor').hasValue()) { + var textAnchor = this.attribute('text-anchor').value; + ctx.textAlign = textAnchor == 'middle' ? 'center' : textAnchor; + } + if (this.attribute('alignment-baseline').hasValue()) ctx.textBaseline = this.attribute('alignment-baseline').value; + } + + this.renderChildren = function(ctx) { + if(this.attribute('visibility').value=='hidden') return; + + var x = this.attribute('x').Length.toPixels('x'); + var y = this.attribute('y').Length.toPixels('y'); + + for (var i=0; i<this.children.length; i++) { + var child = this.children[i]; + + if (child.attribute('x').hasValue()) { + child.x = child.attribute('x').Length.toPixels('x'); + } + else { + if (child.attribute('dx').hasValue()) x += child.attribute('dx').Length.toPixels('x'); + child.x = x; + x += child.measureText(ctx); + } + + if (child.attribute('y').hasValue()) { + child.y = child.attribute('y').Length.toPixels('y'); + } + else { + if (child.attribute('dy').hasValue()) y += child.attribute('dy').Length.toPixels('y'); + child.y = y; + } + + child.render(ctx); + } + } + } + svg.Element.text.prototype = new svg.Element.RenderedElementBase; + + // text base + svg.Element.TextElementBase = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.renderChildren = function(ctx) { + ctx.fillText(svg.compressSpaces(this.getText()), this.x, this.y); + } + + this.getText = function() { + // OVERRIDE ME + } + + this.measureText = function(ctx) { + var textToMeasure = svg.compressSpaces(this.getText()); + if (!ctx.measureText) return textToMeasure.length * 10; + return ctx.measureText(textToMeasure).width; + } + } + svg.Element.TextElementBase.prototype = new svg.Element.RenderedElementBase; + + // tspan + svg.Element.tspan = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + // TEXT ELEMENT + this.text = node.nodeType == 3 ? node.nodeValue : node.childNodes[0].nodeValue; + this.getText = function() { + return this.text; + } + } + svg.Element.tspan.prototype = new svg.Element.TextElementBase; + + // tref + svg.Element.tref = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.getText = function() { + var element = this.attribute('xlink:href').Definition.getDefinition(); + if (element != null) return element.children[0].getText(); + } + } + svg.Element.tref.prototype = new svg.Element.TextElementBase; + + // a element + svg.Element.a = function(node) { + this.base = svg.Element.TextElementBase; + this.base(node); + + this.hasText = true; + for (var i=0; i<node.childNodes.length; i++) { + if (node.childNodes[i].nodeType != 3) this.hasText = false; + } + + // this might contain text + this.text = this.hasText ? node.childNodes[0].nodeValue : ''; + this.getText = function() { + return this.text; + } + + this.baseRenderChildren = this.renderChildren; + this.renderChildren = function(ctx) { + if (this.hasText) { + // render as text element + this.baseRenderChildren(ctx); + var fontSize = new svg.Property('fontSize', svg.Font.Parse(svg.ctx.font).fontSize); + svg.Mouse.checkBoundingBox(this, new svg.BoundingBox(this.x, this.y - fontSize.Length.toPixels('y'), this.x + this.measureText(ctx), this.y)); + } + else { + // render as temporary group + var g = new svg.Element.g(); + g.children = this.children; + g.parent = this; + g.render(ctx); + } + } + + this.onclick = function() { + window.open(this.attribute('xlink:href').value); + } + + this.onmousemove = function() { + svg.ctx.canvas.style.cursor = 'pointer'; + } + } + svg.Element.a.prototype = new svg.Element.TextElementBase; + + // image element + svg.Element.image = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + svg.Images.push(this); + this.img = document.createElement('img'); + this.loaded = false; + var that = this; + this.img.onload = function() { that.loaded = true; } + this.img.src = this.attribute('xlink:href').value; + + this.renderChildren = function(ctx) { + var x = this.attribute('x').Length.toPixels('x'); + var y = this.attribute('y').Length.toPixels('y'); + + var width = this.attribute('width').Length.toPixels('x'); + var height = this.attribute('height').Length.toPixels('y'); + if (width == 0 || height == 0) return; + + ctx.save(); + ctx.translate(x, y); + svg.AspectRatio(ctx, + this.attribute('preserveAspectRatio').value, + width, + this.img.width, + height, + this.img.height, + 0, + 0); + ctx.drawImage(this.img, 0, 0); + ctx.restore(); + } + } + svg.Element.image.prototype = new svg.Element.RenderedElementBase; + + // group element + svg.Element.g = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.getBoundingBox = function() { + var bb = new svg.BoundingBox(); + for (var i=0; i<this.children.length; i++) { + bb.addBoundingBox(this.children[i].getBoundingBox()); + } + return bb; + }; + } + svg.Element.g.prototype = new svg.Element.RenderedElementBase; + + // symbol element + svg.Element.symbol = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.baseSetContext = this.setContext; + this.setContext = function(ctx) { + this.baseSetContext(ctx); + + // viewbox + if (this.attribute('viewBox').hasValue()) { + var viewBox = svg.ToNumberArray(this.attribute('viewBox').value); + var minX = viewBox[0]; + var minY = viewBox[1]; + width = viewBox[2]; + height = viewBox[3]; + + svg.AspectRatio(ctx, + this.attribute('preserveAspectRatio').value, + this.attribute('width').Length.toPixels('x'), + width, + this.attribute('height').Length.toPixels('y'), + height, + minX, + minY); + + svg.ViewPort.SetCurrent(viewBox[2], viewBox[3]); + } + } + } + svg.Element.symbol.prototype = new svg.Element.RenderedElementBase; + + // style element + svg.Element.style = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + var css = node.childNodes[0].nodeValue; + css = css.replace(/(/*([^*]|[\r\n]|(*+([^*/]|[\r\n])))**+/)|(//.*)/gm, ''); // remove comments + css = svg.compressSpaces(css); // replace whitespace + var cssDefs = css.split('}'); + for (var i=0; i<cssDefs.length; i++) { + if (svg.trim(cssDefs[i]) != '') { + var cssDef = cssDefs[i].split('{'); + var cssClasses = cssDef[0].split(','); + var cssProps = cssDef[1].split(';'); + for (var j=0; j<cssClasses.length; j++) { + var cssClass = svg.trim(cssClasses[j]); + if (cssClass != '') { + var props = {}; + for (var k=0; k<cssProps.length; k++) { + var prop = cssProps[k].split(':'); + var name = prop[0]; + var value = prop[1]; + if (name != null && value != null) { + props[svg.trim(prop[0])] = new svg.Property(svg.trim(prop[0]), svg.trim(prop[1])); + } + } + svg.Styles[cssClass] = props; + } + } + } + } + } + svg.Element.style.prototype = new svg.Element.ElementBase; + + // use element + svg.Element.use = function(node) { + this.base = svg.Element.RenderedElementBase; + this.base(node); + + this.baseSetContext = this.setContext; + this.setContext = function(ctx) { + this.baseSetContext(ctx); + if (this.attribute('x').hasValue()) ctx.translate(this.attribute('x').Length.toPixels('x'), 0); + if (this.attribute('y').hasValue()) ctx.translate(0, this.attribute('y').Length.toPixels('y')); + } + + this.getDefinition = function() { + var element = this.attribute('xlink:href').Definition.getDefinition(); + if (this.attribute('width').hasValue()) element.attribute('width', true).value = this.attribute('width').value; + if (this.attribute('height').hasValue()) element.attribute('height', true).value = this.attribute('height').value; + return element; + } + + this.path = function(ctx) { + var element = this.getDefinition(); + if (element != null) element.path(ctx); + } + + this.renderChildren = function(ctx) { + var element = this.getDefinition(); + if (element != null) element.render(ctx); + } + } + svg.Element.use.prototype = new svg.Element.RenderedElementBase; + + // clip element + svg.Element.clipPath = function(node) { + this.base = svg.Element.ElementBase; + this.base(node); + + this.apply = function(ctx) { + for (var i=0; i<this.children.length; i++) { + if (this.children[i].path) { + this.children[i].path(ctx); + ctx.clip(); + } + } + } + } + svg.Element.clipPath.prototype = new svg.Element.ElementBase; + + // title element, do nothing + svg.Element.title = function(node) { + } + svg.Element.title.prototype = new svg.Element.ElementBase; + + // desc element, do nothing + svg.Element.desc = function(node) { + } + svg.Element.desc.prototype = new svg.Element.ElementBase; + + svg.Element.MISSING = function(node) { + console.log('ERROR: Element '' + node.nodeName + '' not yet implemented.'); + } + svg.Element.MISSING.prototype = new svg.Element.ElementBase; + + // element factory + svg.CreateElement = function(node) { + var className = node.nodeName.replace(/^[^:]+:/,''); + var e = null; + if (typeof(svg.Element[className]) != 'undefined') { + e = new svg.Element[className](node); + } + else { + e = new svg.Element.MISSING(node); + } + + e.type = node.nodeName; + return e; + } + + // load from url + svg.load = function(ctx, url) { + svg.loadXml(ctx, svg.ajax(url)); + } + + // load from xml + svg.loadXml = function(ctx, xml) { + svg.init(ctx); + + var mapXY = function(p) { + var e = ctx.canvas; + while (e) { + p.x -= e.offsetLeft; + p.y -= e.offsetTop; + e = e.offsetParent; + } + if (window.scrollX) p.x += window.scrollX; + if (window.scrollY) p.y += window.scrollY; + return p; + } + + // bind mouse + if (svg.opts == null || svg.opts['ignoreMouse'] != true) { + ctx.canvas.onclick = function(e) { + var p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); + svg.Mouse.onclick(p.x, p.y); + }; + ctx.canvas.onmousemove = function(e) { + var p = mapXY(new svg.Point(e != null ? e.clientX : event.clientX, e != null ? e.clientY : event.clientY)); + svg.Mouse.onmousemove(p.x, p.y); + }; + } + + var dom = svg.parseXml(xml); + var e = svg.CreateElement(dom.documentElement); + + // render loop + var isFirstRender = true; + var draw = function() { + if (svg.opts == null || svg.opts['ignoreDimensions'] != true) { + // set canvas size + if (e.style('width').hasValue()) { + ctx.canvas.width = e.style('width').Length.toPixels(ctx.canvas.parentNode.clientWidth); + } + if (e.style('height').hasValue()) { + ctx.canvas.height = e.style('height').Length.toPixels(ctx.canvas.parentNode.clientHeight); + } + } + svg.ViewPort.SetCurrent(ctx.canvas.clientWidth, ctx.canvas.clientHeight); + + if (svg.opts != null && svg.opts['offsetX'] != null) e.attribute('x', true).value = svg.opts['offsetX']; + if (svg.opts != null && svg.opts['offsetY'] != null) e.attribute('y', true).value = svg.opts['offsetY']; + if (svg.opts != null && svg.opts['scaleWidth'] != null && svg.opts['scaleHeight'] != null) { + e.attribute('width', true).value = svg.opts['scaleWidth']; + e.attribute('height', true).value = svg.opts['scaleHeight']; + e.attribute('viewBox', true).value = '0 0 ' + ctx.canvas.clientWidth + ' ' + ctx.canvas.clientHeight; + e.attribute('preserveAspectRatio', true).value = 'none'; + } + + // clear and render + if (svg.opts == null || svg.opts['ignoreClear'] != true) { + ctx.clearRect(0, 0, ctx.canvas.clientWidth, ctx.canvas.clientHeight); + } + e.render(ctx); + if (isFirstRender) { + isFirstRender = false; + if (svg.opts != null && typeof(svg.opts['renderCallback']) == 'function') svg.opts['renderCallback'](); + } + } + + var waitingForImages = true; + if (svg.ImagesLoaded()) { + waitingForImages = false; + draw(); + } + svg.intervalID = setInterval(function() { + var needUpdate = false; + + if (waitingForImages && svg.ImagesLoaded()) { + waitingForImages = false; + needUpdate = true; + } + + // need update from mouse events? + if (svg.opts == null || svg.opts['ignoreMouse'] != true) { + needUpdate = needUpdate | svg.Mouse.hasEvents(); + } + + // need update from animations? + if (svg.opts == null || svg.opts['ignoreAnimation'] != true) { + for (var i=0; i<svg.Animations.length; i++) { + needUpdate = needUpdate | svg.Animations[i].update(1000 / svg.FRAMERATE); + } + } + + // need update from redraw? + if (svg.opts != null && typeof(svg.opts['forceRedraw']) == 'function') { + if (svg.opts['forceRedraw']() == true) needUpdate = true; + } + + // render if needed + if (needUpdate) { + draw(); + svg.Mouse.runEvents(); // run and clear our events + } + }, 1000 / svg.FRAMERATE); + } + + svg.stop = function() { + if (svg.intervalID) { + clearInterval(svg.intervalID); + } + } + + svg.Mouse = new (function() { + this.events = []; + this.hasEvents = function() { return this.events.length != 0; } + + this.onclick = function(x, y) { + this.events.push({ type: 'onclick', x: x, y: y, + run: function(e) { if (e.onclick) e.onclick(); } + }); + } + + this.onmousemove = function(x, y) { + this.events.push({ type: 'onmousemove', x: x, y: y, + run: function(e) { if (e.onmousemove) e.onmousemove(); } + }); + } + + this.eventElements = []; + + this.checkPath = function(element, ctx) { + for (var i=0; i<this.events.length; i++) { + var e = this.events[i]; + if (ctx.isPointInPath && ctx.isPointInPath(e.x, e.y)) this.eventElements[i] = element; + } + } + + this.checkBoundingBox = function(element, bb) { + for (var i=0; i<this.events.length; i++) { + var e = this.events[i]; + if (bb.isPointInBox(e.x, e.y)) this.eventElements[i] = element; + } + } + + this.runEvents = function() { + svg.ctx.canvas.style.cursor = ''; + + for (var i=0; i<this.events.length; i++) { + var e = this.events[i]; + var element = this.eventElements[i]; + while (element) { + e.run(element); + element = element.parent; + } + } + + // done running, clear + this.events = []; + this.eventElements = []; + } + }); + + return svg; + } +})(); + +if (CanvasRenderingContext2D) { + CanvasRenderingContext2D.prototype.drawSvg = function(s, dx, dy, dw, dh) { + canvg(this.canvas, s, { + ignoreMouse: true, + ignoreAnimation: true, + ignoreDimensions: true, + ignoreClear: true, + offsetX: dx, + offsetY: dy, + scaleWidth: dw, + scaleHeight: dh + }); + } +} \ No newline at end of file diff --git a/js/canvg/rgbcolor.js b/js/canvg/rgbcolor.js new file mode 100644 index 0000000..0338a16 --- /dev/null +++ b/js/canvg/rgbcolor.js @@ -0,0 +1,288 @@ +/** + * A class to parse color values + * @author Stoyan Stefanov sstoo@gmail.com + * @link http://www.phpied.com/rgb-color-parser-in-javascript/ + * @license Use it if you like it + */ +function RGBColor(color_string) +{ + this.ok = false; + + // strip any leading # + if (color_string.charAt(0) == '#') { // remove # if any + color_string = color_string.substr(1,6); + } + + color_string = color_string.replace(/ /g,''); + color_string = color_string.toLowerCase(); + + // before getting into regexps, try simple matches + // and overwrite the input + var simple_colors = { + aliceblue: 'f0f8ff', + antiquewhite: 'faebd7', + aqua: '00ffff', + aquamarine: '7fffd4', + azure: 'f0ffff', + beige: 'f5f5dc', + bisque: 'ffe4c4', + black: '000000', + blanchedalmond: 'ffebcd', + blue: '0000ff', + blueviolet: '8a2be2', + brown: 'a52a2a', + burlywood: 'deb887', + cadetblue: '5f9ea0', + chartreuse: '7fff00', + chocolate: 'd2691e', + coral: 'ff7f50', + cornflowerblue: '6495ed', + cornsilk: 'fff8dc', + crimson: 'dc143c', + cyan: '00ffff', + darkblue: '00008b', + darkcyan: '008b8b', + darkgoldenrod: 'b8860b', + darkgray: 'a9a9a9', + darkgreen: '006400', + darkkhaki: 'bdb76b', + darkmagenta: '8b008b', + darkolivegreen: '556b2f', + darkorange: 'ff8c00', + darkorchid: '9932cc', + darkred: '8b0000', + darksalmon: 'e9967a', + darkseagreen: '8fbc8f', + darkslateblue: '483d8b', + darkslategray: '2f4f4f', + darkturquoise: '00ced1', + darkviolet: '9400d3', + deeppink: 'ff1493', + deepskyblue: '00bfff', + dimgray: '696969', + dodgerblue: '1e90ff', + feldspar: 'd19275', + firebrick: 'b22222', + floralwhite: 'fffaf0', + forestgreen: '228b22', + fuchsia: 'ff00ff', + gainsboro: 'dcdcdc', + ghostwhite: 'f8f8ff', + gold: 'ffd700', + goldenrod: 'daa520', + gray: '808080', + green: '008000', + greenyellow: 'adff2f', + honeydew: 'f0fff0', + hotpink: 'ff69b4', + indianred : 'cd5c5c', + indigo : '4b0082', + ivory: 'fffff0', + khaki: 'f0e68c', + lavender: 'e6e6fa', + lavenderblush: 'fff0f5', + lawngreen: '7cfc00', + lemonchiffon: 'fffacd', + lightblue: 'add8e6', + lightcoral: 'f08080', + lightcyan: 'e0ffff', + lightgoldenrodyellow: 'fafad2', + lightgrey: 'd3d3d3', + lightgreen: '90ee90', + lightpink: 'ffb6c1', + lightsalmon: 'ffa07a', + lightseagreen: '20b2aa', + lightskyblue: '87cefa', + lightslateblue: '8470ff', + lightslategray: '778899', + lightsteelblue: 'b0c4de', + lightyellow: 'ffffe0', + lime: '00ff00', + limegreen: '32cd32', + linen: 'faf0e6', + magenta: 'ff00ff', + maroon: '800000', + mediumaquamarine: '66cdaa', + mediumblue: '0000cd', + mediumorchid: 'ba55d3', + mediumpurple: '9370d8', + mediumseagreen: '3cb371', + mediumslateblue: '7b68ee', + mediumspringgreen: '00fa9a', + mediumturquoise: '48d1cc', + mediumvioletred: 'c71585', + midnightblue: '191970', + mintcream: 'f5fffa', + mistyrose: 'ffe4e1', + moccasin: 'ffe4b5', + navajowhite: 'ffdead', + navy: '000080', + oldlace: 'fdf5e6', + olive: '808000', + olivedrab: '6b8e23', + orange: 'ffa500', + orangered: 'ff4500', + orchid: 'da70d6', + palegoldenrod: 'eee8aa', + palegreen: '98fb98', + paleturquoise: 'afeeee', + palevioletred: 'd87093', + papayawhip: 'ffefd5', + peachpuff: 'ffdab9', + peru: 'cd853f', + pink: 'ffc0cb', + plum: 'dda0dd', + powderblue: 'b0e0e6', + purple: '800080', + red: 'ff0000', + rosybrown: 'bc8f8f', + royalblue: '4169e1', + saddlebrown: '8b4513', + salmon: 'fa8072', + sandybrown: 'f4a460', + seagreen: '2e8b57', + seashell: 'fff5ee', + sienna: 'a0522d', + silver: 'c0c0c0', + skyblue: '87ceeb', + slateblue: '6a5acd', + slategray: '708090', + snow: 'fffafa', + springgreen: '00ff7f', + steelblue: '4682b4', + tan: 'd2b48c', + teal: '008080', + thistle: 'd8bfd8', + tomato: 'ff6347', + turquoise: '40e0d0', + violet: 'ee82ee', + violetred: 'd02090', + wheat: 'f5deb3', + white: 'ffffff', + whitesmoke: 'f5f5f5', + yellow: 'ffff00', + yellowgreen: '9acd32' + }; + for (var key in simple_colors) { + if (color_string == key) { + color_string = simple_colors[key]; + } + } + // emd of simple type-in colors + + // array of color definition objects + var color_defs = [ + { + re: /^rgb((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3}))$/, + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], + process: function (bits){ + return [ + parseInt(bits[1]), + parseInt(bits[2]), + parseInt(bits[3]) + ]; + } + }, + { + re: /^(\w{2})(\w{2})(\w{2})$/, + example: ['#00ff00', '336699'], + process: function (bits){ + return [ + parseInt(bits[1], 16), + parseInt(bits[2], 16), + parseInt(bits[3], 16) + ]; + } + }, + { + re: /^(\w{1})(\w{1})(\w{1})$/, + example: ['#fb0', 'f0f'], + process: function (bits){ + return [ + parseInt(bits[1] + bits[1], 16), + parseInt(bits[2] + bits[2], 16), + parseInt(bits[3] + bits[3], 16) + ]; + } + } + ]; + + // search through the definitions to find a match + for (var i = 0; i < color_defs.length; i++) { + var re = color_defs[i].re; + var processor = color_defs[i].process; + var bits = re.exec(color_string); + if (bits) { + channels = processor(bits); + this.r = channels[0]; + this.g = channels[1]; + this.b = channels[2]; + this.ok = true; + } + + } + + // validate/cleanup values + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); + + // some getters + this.toRGB = function () { + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; + } + this.toHex = function () { + var r = this.r.toString(16); + var g = this.g.toString(16); + var b = this.b.toString(16); + if (r.length == 1) r = '0' + r; + if (g.length == 1) g = '0' + g; + if (b.length == 1) b = '0' + b; + return '#' + r + g + b; + } + + // help + this.getHelpXML = function () { + + var examples = new Array(); + // add regexps + for (var i = 0; i < color_defs.length; i++) { + var example = color_defs[i].example; + for (var j = 0; j < example.length; j++) { + examples[examples.length] = example[j]; + } + } + // add type-in colors + for (var sc in simple_colors) { + examples[examples.length] = sc; + } + + var xml = document.createElement('ul'); + xml.setAttribute('id', 'rgbcolor-examples'); + for (var i = 0; i < examples.length; i++) { + try { + var list_item = document.createElement('li'); + var list_color = new RGBColor(examples[i]); + var example_div = document.createElement('div'); + example_div.style.cssText = + 'margin: 3px; ' + + 'border: 1px solid black; ' + + 'background:' + list_color.toHex() + '; ' + + 'color:' + list_color.toHex() + ; + example_div.appendChild(document.createTextNode('test')); + var list_item_value = document.createTextNode( + ' ' + examples[i] + ' -> ' + list_color.toRGB() + ' -> ' + list_color.toHex() + ); + list_item.appendChild(example_div); + list_item.appendChild(list_item_value); + xml.appendChild(list_item); + + } catch(e){} + } + return xml; + + } + +} + diff --git a/js/functions.js b/js/functions.js index 765f887..6266ba6 100644 --- a/js/functions.js +++ b/js/functions.js @@ -26,6 +26,13 @@ var ajax_message_init = false; var codemirror_editor = false;
/** + * @var chart_activeTimeouts object active timeouts that refresh the charts. When disabling a realtime chart, this can be used to stop the continuous ajax requests + */ +var chart_activeTimeouts = new Object(); + + + +/** * Add a hidden field to the form to indicate that this will be an * Ajax request (only if this hidden field does not exist) * @@ -1237,6 +1244,7 @@ $(document).ready(function(){ * optional, defaults to 'Loading...' * @param var timeout number of milliseconds for the message to be visible * optional, defaults to 5000 + * @return jQuery object jQuery Element that holds the message div */
function PMA_ajaxShowMessage(message, timeout) { @@ -1301,7 +1309,7 @@ function PMA_ajaxShowMessage(message, timeout) { }) }
- return $("#loading"); + return $("#loading"); }
/** @@ -1332,7 +1340,7 @@ function PMA_showNoticeForEnum(selectElement) { /** * Generates a dialog box to pop up the create_table form */ -function PMA_createTableDialog( div, url , target){ +function PMA_createTableDialog( div, url , target) { /** * @var button_options Object that stores the options passed to jQueryUI * dialog @@ -1377,6 +1385,119 @@ function PMA_createTableDialog( div, url , target){ }
/** + * Creates a highcharts chart in the given container + * + * @param var settings object with highcharts properties that should be applied. (See also http://www.highcharts.com/ref/) + * requires at least settings.chart.renderTo and settings.series to be set. + * In addition there may be an additional property object 'realtime' that allows for realtime charting: + * realtime: { + * url: adress to get the data from (will always add token, ajax_request=1 and chart_data=1 to the GET request) + * type: the GET request will also add type=[value of the type property] to the request + * callback: Callback function that should draw the point, it's called with 4 parameters in this order: + * - the chart object + * - the current response value of the GET request, JSON parsed + * - the previous response value of the GET request, JSON parsed + * - the number of added points + * + * @return object The created highcharts instance + */ +function PMA_createChart(passedSettings) { + var container = passedSettings.chart.renderTo; + + var settings = { + chart: { + type: 'spline', + marginRight: 10, + events: { + load: function() { + var thisChart = this; + var lastValue=null, curValue=null; + var numLoadedPoints=0, otherSum=0; + var diff; + // No realtime updates for graphs that are being exported, and disabled when no callback is set + if(thisChart.options.chart.forExport==true || !passedSettings.realtime || !passedSettings.realtime.callback) return; + + thisChart.options.realtime.timeoutCallBack = function() { + $.get(passedSettings.realtime.url,{ajax_request:1, chart_data:1, type:passedSettings.realtime.type},function(data) { + if(chart_activeTimeouts[container]==null) return; + + curValue = jQuery.parseJSON(data); + //if(lastValue==null) lastValue = curValue; + + if(lastValue==null) diff = curValue.x - thisChart.xAxis[0].getExtremes().max; + else diff = parseInt(curValue.x - lastValue.x); + + thisChart.xAxis[0].setExtremes(thisChart.xAxis[0].getExtremes().min+diff, thisChart.xAxis[0].getExtremes().max+diff, false); + + passedSettings.realtime.callback(thisChart,curValue,lastValue,numLoadedPoints); + + lastValue = curValue; + numLoadedPoints++; + chart_activeTimeouts[container] = setTimeout(thisChart.options.realtime.timeoutCallBack, thisChart.options.realtime.refreshRate); + + }); + } + + chart_activeTimeouts[container] = setTimeout(thisChart.options.realtime.timeoutCallBack, 0); + } + } + }, + plotOptions: { + series: { + marker: { + radius: 3 + } + } + }, + credits: { + enabled:false + }, + xAxis: { + type: 'datetime', + }, + yAxis: { + min: 0, + title: { + text: PMA_messages['strTotalCount'] + }, + plotLines: [{ + value: 0, + width: 1, + color: '#808080' + }] + }, + tooltip: { + formatter: function() { + return '<b>'+ this.series.name +'</b><br/>'+ + Highcharts.dateFormat('%Y-%m-%d %H:%M:%S', this.x) +'<br/>'+ + Highcharts.numberFormat(this.y, 2); + } + }, + exporting: { + enabled: true + }, + series: [] + } + + /* Set/Get realtime chart default values */ + if(passedSettings.realtime) { + if(!passedSettings.realtime.refreshRate) + passedSettings.realtime.refreshRate = 5000; + + if(!passedSettings.realtime.numMaxPoints) + passedSettings.realtime.numMaxPoints = 32; + + settings.xAxis.min = new Date().getTime() - passedSettings.realtime.numMaxPoints * passedSettings.realtime.refreshRate; + settings.xAxis.max = new Date().getTime() + passedSettings.realtime.refreshRate / 2; + } + + // Overwrite/Merge default settings with passedsettings + $.extend(true,settings,passedSettings); + + return new Highcharts.Chart(settings); +} + +/** * jQuery function that uses jQueryUI's dialogs to confirm with user. Does not * return a jQuery object yet and hence cannot be chained * @@ -2006,8 +2127,10 @@ $(document).ready(function() { } });
+$(document).ready(initTooltips); + /* Displays tooltips */ -$(document).ready(function() { +function initTooltips() { // Hide the footnotes from the footer (which are displayed for // JavaScript-disabled browsers) since the tooltip is sufficient $(".footnotes").hide(); @@ -2033,7 +2156,7 @@ $(document).ready(function() { style: { background: '#ffffcc' } }); }); -}); +}
function menuResize() { diff --git a/js/highcharts/exporting.js b/js/highcharts/exporting.js new file mode 100644 index 0000000..4f134d5 --- /dev/null +++ b/js/highcharts/exporting.js @@ -0,0 +1,690 @@ +/** + * @license Highcharts JS v2.1.4 (2011-03-02) + * Exporting module + * + * (c) 2010 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*global Highcharts, document, window, Math, setTimeout */ + +(function() { // encapsulate + +// create shortcuts +var HC = Highcharts, + Chart = HC.Chart, + addEvent = HC.addEvent, + createElement = HC.createElement, + discardElement = HC.discardElement, + css = HC.css, + merge = HC.merge, + each = HC.each, + extend = HC.extend, + math = Math, + mathMax = math.max, + doc = document, + win = window, + hasTouch = 'ontouchstart' in doc.documentElement, + M = 'M', + L = 'L', + DIV = 'div', + HIDDEN = 'hidden', + NONE = 'none', + PREFIX = 'highcharts-', + ABSOLUTE = 'absolute', + PX = 'px', + + + + // Add language and get the defaultOptions + defaultOptions = HC.setOptions({ + lang: { + downloadPNG: 'Download PNG image', + downloadJPEG: 'Download JPEG image', + downloadPDF: 'Download PDF document', + downloadSVG: 'Download SVG vector image', + exportButtonTitle: 'Export to raster or vector image', + printButtonTitle: 'Print the chart' + } + }); + +// Buttons and menus are collected in a separate config option set called 'navigation'. +// This can be extended later to add control buttons like zoom and pan right click menus. +defaultOptions.navigation = { + menuStyle: { + border: '1px solid #A0A0A0', + background: '#FFFFFF' + }, + menuItemStyle: { + padding: '0 5px', + background: NONE, + color: '#303030', + fontSize: hasTouch ? '14px' : '11px' + }, + menuItemHoverStyle: { + background: '#4572A5', + color: '#FFFFFF' + }, + + buttonOptions: { + align: 'right', + backgroundColor: { + linearGradient: [0, 0, 0, 20], + stops: [ + [0.4, '#F7F7F7'], + [0.6, '#E3E3E3'] + ] + }, + borderColor: '#B0B0B0', + borderRadius: 3, + borderWidth: 1, + //enabled: true, + height: 20, + hoverBorderColor: '#909090', + hoverSymbolFill: '#81A7CF', + hoverSymbolStroke: '#4572A5', + symbolFill: '#E0E0E0', + //symbolSize: 12, + symbolStroke: '#A0A0A0', + //symbolStrokeWidth: 1, + symbolX: 11.5, + symbolY: 10.5, + verticalAlign: 'top', + width: 24, + y: 10 + } +}; + + + +// Add the export related options +defaultOptions.exporting = { + //enabled: true, + //filename: 'chart', + type: 'image/png', + url: 'chart_export.php', + width: 800, + buttons: { + exportButton: { + //enabled: true, + symbol: 'exportIcon', + x: -10, + symbolFill: '#A8BF77', + hoverSymbolFill: '#768F3E', + _titleKey: 'exportButtonTitle', + menuItems: [{ + textKey: 'downloadPNG', + onclick: function() { + this.exportChart(); + } + },/* { + textKey: 'downloadJPEG', + onclick: function() { + this.exportChart({ + type: 'image/jpeg' + }); + } + }, { + textKey: 'downloadPDF', + onclick: function() { + this.exportChart({ + type: 'application/pdf' + }); + } + }, */{ + textKey: 'downloadSVG', + onclick: function() { + this.exportChart({ + type: 'image/svg+xml' + }); + } + }/*, { + text: 'View SVG', + onclick: function() { + var svg = this.getSVG() + .replace(/</g, '\n<') + .replace(/>/g, '>'); + + doc.body.innerHTML = '<pre>'+ svg +'</pre>'; + } + }*/] + + }, + printButton: { + //enabled: true, + symbol: 'printIcon', + x: -36, + symbolFill: '#B5C9DF', + hoverSymbolFill: '#779ABF', + _titleKey: 'printButtonTitle', + onclick: function() { + this.print(); + } + } + } +}; + + + +extend(Chart.prototype, { + /** + * Return an SVG representation of the chart + * + * @param additionalOptions {Object} Additional chart options for the generated SVG representation + */ + getSVG: function(additionalOptions) { + var chart = this, + chartCopy, + sandbox, + svg, + seriesOptions, + config, + pointOptions, + pointMarker, + options = merge(chart.options, additionalOptions); // copy the options and add extra options + + // IE compatibility hack for generating SVG content that it doesn't really understand + if (!doc.createElementNS) { + doc.createElementNS = function(ns, tagName) { + var elem = doc.createElement(tagName); + elem.getBBox = function() { + return chart.renderer.Element.prototype.getBBox.apply({ element: elem }); + }; + return elem; + }; + } + + // create a sandbox where a new chart will be generated + sandbox = createElement(DIV, null, { + position: ABSOLUTE, + top: '-9999em', + width: chart.chartWidth + PX, + height: chart.chartHeight + PX + }, doc.body); + + // override some options + extend(options.chart, { + renderTo: sandbox, + forExport: true + }); + options.exporting.enabled = false; // hide buttons in print + options.chart.plotBackgroundImage = null; // the converter doesn't handle images + // prepare for replicating the chart + options.series = []; + each(chart.series, function(serie) { + seriesOptions = serie.options; + + seriesOptions.animation = false; // turn off animation + seriesOptions.showCheckbox = false; + + // remove image markers + if (seriesOptions && seriesOptions.marker && /^url(/.test(seriesOptions.marker.symbol)) { + seriesOptions.marker.symbol = 'circle'; + } + + seriesOptions.data = []; + + each(serie.data, function(point) { + + // extend the options by those values that can be expressed in a number or array config + config = point.config; + pointOptions = { + x: point.x, + y: point.y, + name: point.name + }; + + if (typeof config == 'object' && point.config && config.constructor != Array) { + extend(pointOptions, config); + } + + seriesOptions.data.push(pointOptions); // copy fresh updated data + + // remove image markers + pointMarker = point.config && point.config.marker; + if (pointMarker && /^url(/.test(pointMarker.symbol)) { + delete pointMarker.symbol; + } + }); + + options.series.push(seriesOptions); + }); + + // generate the chart copy + chartCopy = new Highcharts.Chart(options); + + // get the SVG from the container's innerHTML + svg = chartCopy.container.innerHTML; + + // free up memory + options = null; + chartCopy.destroy(); + discardElement(sandbox); + + // sanitize + svg = svg + .replace(/zIndex="[^"]+"/g, '') + .replace(/isShadow="[^"]+"/g, '') + .replace(/symbolName="[^"]+"/g, '') + .replace(/jQuery[0-9]+="[^"]+"/g, '') + .replace(/isTracker="[^"]+"/g, '') + .replace(/url([^#]+#/g, 'url(#') + /*.replace(/<svg /, '<svg xmlns:xlink="http://www.w3.org/1999/xlink" ') + .replace(/ href=/, ' xlink:href=') + .replace(/preserveAspectRatio="none">/g, 'preserveAspectRatio="none"/>')*/ + /* This fails in IE < 8 + .replace(/([0-9]+).([0-9]+)/g, function(s1, s2, s3) { // round off to save weight + return s2 +'.'+ s3[0]; + })*/ + + // IE specific + .replace(/id=([^" >]+)/g, 'id="$1"') + .replace(/class=([^" ]+)/g, 'class="$1"') + .replace(/ transform /g, ' ') + .replace(/:(path|rect)/g, '$1') + .replace(/style="([^"]+)"/g, function(s) { + return s.toLowerCase(); + }); + + // IE9 beta bugs with innerHTML. Test again with final IE9. + svg = svg.replace(/(url(#highcharts-[0-9]+)"/g, '$1') + .replace(/"/g, "'"); + if (svg.match(/ xmlns="/g).length == 2) { + svg = svg.replace(/xmlns="[^"]+"/, ''); + } + + return svg; + }, + + /** + * Submit the SVG representation of the chart to the server + * @param {Object} options Exporting options. Possible members are url, type and width. + * @param {Object} chartOptions Additional chart options for the SVG representation of the chart + */ + exportChart: function(options, chartOptions) { + var form, + chart = this, + canvas=createElement('canvas'); + + $('body').append(canvas); + $(canvas).hide(); + + var submitData = function(chartData) { + // merge the options + options = merge(chart.options.exporting, options); + + // create the form + form = createElement('form', { + method: 'post', + action: options.url + }, { + display: NONE + }, doc.body); + + // add the values + each(['filename', 'type', 'width', 'image','token'], function(name) { + createElement('input', { + type: HIDDEN, + name: name, + value: { + filename: options.filename || 'chart', + type: options.type, + width: options.width, + image: chartData, + token: pma_token + }[name] + }, null, form); + }); + + // submit + form.submit(); + + // clean up + discardElement(form); + } + + if(options && options.type=='image/svg+xml') { + submitData(chart.getSVG(chartOptions)); + } else { + // Generate data uri and submit once done + canvg(canvas, chart.getSVG(chartOptions),{ + ignoreAnimation:true, + ignoreMouse:true, + renderCallback:function() { submitData(canvas.toDataURL()); } + }); + } + }, + + /** + * Print the chart + */ + print: function() { + + var chart = this, + container = chart.container, + origDisplay = [], + origParent = container.parentNode, + body = doc.body, + childNodes = body.childNodes; + + if (chart.isPrinting) { // block the button while in printing mode + return; + } + + chart.isPrinting = true; + + // hide all body content + each(childNodes, function(node, i) { + if (node.nodeType == 1) { + origDisplay[i] = node.style.display; + node.style.display = NONE; + } + }); + + // pull out the chart + body.appendChild(container); + + // print + win.print(); + + // allow the browser to prepare before reverting + setTimeout(function() { + + // put the chart back in + origParent.appendChild(container); + + // restore all body content + each(childNodes, function(node, i) { + if (node.nodeType == 1) { + node.style.display = origDisplay[i]; + } + }); + + chart.isPrinting = false; + + }, 1000); + + }, + + /** + * Display a popup menu for choosing the export type + * + * @param {String} name An identifier for the menu + * @param {Array} items A collection with text and onclicks for the items + * @param {Number} x The x position of the opener button + * @param {Number} y The y position of the opener button + * @param {Number} width The width of the opener button + * @param {Number} height The height of the opener button + */ + contextMenu: function(name, items, x, y, width, height) { + var chart = this, + navOptions = chart.options.navigation, + menuItemStyle = navOptions.menuItemStyle, + chartWidth = chart.chartWidth, + chartHeight = chart.chartHeight, + cacheName = 'cache-'+ name, + menu = chart[cacheName], + menuPadding = mathMax(width, height), // for mouse leave detection + boxShadow = '3px 3px 10px #888', + innerMenu, + hide, + menuStyle; + + // create the menu only the first time + if (!menu) { + + // create a HTML element above the SVG + chart[cacheName] = menu = createElement(DIV, { + className: PREFIX + name + }, { + position: ABSOLUTE, + zIndex: 1000, + padding: menuPadding + PX + }, chart.container); + + innerMenu = createElement(DIV, null, + extend({ + MozBoxShadow: boxShadow, + WebkitBoxShadow: boxShadow, + boxShadow: boxShadow + }, navOptions.menuStyle) , menu); + + // hide on mouse out + hide = function() { + css(menu, { display: NONE }); + }; + + addEvent(menu, 'mouseleave', hide); + + + // create the items + each(items, function(item) { + if (item) { + var div = createElement(DIV, { + onmouseover: function() { + css(this, navOptions.menuItemHoverStyle); + }, + onmouseout: function() { + css(this, menuItemStyle); + }, + innerHTML: item.text || HC.getOptions().lang[item.textKey] + }, extend({ + cursor: 'pointer' + }, menuItemStyle), innerMenu); + + div[hasTouch ? 'ontouchstart' : 'onclick'] = function() { + hide(); + item.onclick.apply(chart, arguments); + }; + + } + }); + + chart.exportMenuWidth = menu.offsetWidth; + chart.exportMenuHeight = menu.offsetHeight; + } + + menuStyle = { display: 'block' }; + + // if outside right, right align it + if (x + chart.exportMenuWidth > chartWidth) { + menuStyle.right = (chartWidth - x - width - menuPadding) + PX; + } else { + menuStyle.left = (x - menuPadding) + PX; + } + // if outside bottom, bottom align it + if (y + height + chart.exportMenuHeight > chartHeight) { + menuStyle.bottom = (chartHeight - y - menuPadding) + PX; + } else { + menuStyle.top = (y + height - menuPadding) + PX; + } + + css(menu, menuStyle); + }, + + /** + * Add the export button to the chart + */ + addButton: function(options) { + var chart = this, + renderer = chart.renderer, + btnOptions = merge(chart.options.navigation.buttonOptions, options), + onclick = btnOptions.onclick, + menuItems = btnOptions.menuItems, + /*position = chart.getAlignment(btnOptions), + buttonLeft = position.x, + buttonTop = position.y,*/ + buttonWidth = btnOptions.width, + buttonHeight = btnOptions.height, + box, + symbol, + button, + borderWidth = btnOptions.borderWidth, + boxAttr = { + stroke: btnOptions.borderColor + + }, + symbolAttr = { + stroke: btnOptions.symbolStroke, + fill: btnOptions.symbolFill + }; + + if (btnOptions.enabled === false) { + return; + } + + // element to capture the click + function revert() { + symbol.attr(symbolAttr); + box.attr(boxAttr); + } + + // the box border + box = renderer.rect( + 0, + 0, + buttonWidth, + buttonHeight, + btnOptions.borderRadius, + borderWidth + ) + //.translate(buttonLeft, buttonTop) // to allow gradients + .align(btnOptions, true) + .attr(extend({ + fill: btnOptions.backgroundColor, + 'stroke-width': borderWidth, + zIndex: 19 + }, boxAttr)).add(); + + // the invisible element to track the clicks + button = renderer.rect( + 0, + 0, + buttonWidth, + buttonHeight, + 0 + ) + .align(btnOptions) + .attr({ + fill: 'rgba(255, 255, 255, 0.001)', + title: HC.getOptions().lang[btnOptions._titleKey], + zIndex: 21 + }).css({ + cursor: 'pointer' + }) + .on('mouseover', function() { + symbol.attr({ + stroke: btnOptions.hoverSymbolStroke, + fill: btnOptions.hoverSymbolFill + }); + box.attr({ + stroke: btnOptions.hoverBorderColor + }); + }) + .on('mouseout', revert) + .on('click', revert) + .add(); + + //addEvent(button.element, 'click', revert); + + // add the click event + if (menuItems) { + onclick = function(e) { + revert(); + var bBox = button.getBBox(); + chart.contextMenu('export-menu', menuItems, bBox.x, bBox.y, buttonWidth, buttonHeight); + }; + } + /*addEvent(button.element, 'click', function() { + onclick.apply(chart, arguments); + });*/ + button.on('click', function() { + onclick.apply(chart, arguments); + }); + + // the icon + symbol = renderer.symbol( + btnOptions.symbol, + btnOptions.symbolX, + btnOptions.symbolY, + (btnOptions.symbolSize || 12) / 2 + ) + .align(btnOptions, true) + .attr(extend(symbolAttr, { + 'stroke-width': btnOptions.symbolStrokeWidth || 1, + zIndex: 20 + })).add(); + + + + } +}); + +// Create the export icon +HC.Renderer.prototype.symbols.exportIcon = function(x, y, radius) { + return [ + M, // the disk + x - radius, y + radius, + L, + x + radius, y + radius, + x + radius, y + radius * 0.5, + x - radius, y + radius * 0.5, + 'Z', + M, // the arrow + x, y + radius * 0.5, + L, + x - radius * 0.5, y - radius / 3, + x - radius / 6, y - radius / 3, + x - radius / 6, y - radius, + x + radius / 6, y - radius, + x + radius / 6, y - radius / 3, + x + radius * 0.5, y - radius / 3, + 'Z' + ]; +}; +// Create the print icon +HC.Renderer.prototype.symbols.printIcon = function(x, y, radius) { + return [ + M, // the printer + x - radius, y + radius * 0.5, + L, + x + radius, y + radius * 0.5, + x + radius, y - radius / 3, + x - radius, y - radius / 3, + 'Z', + M, // the upper sheet + x - radius * 0.5, y - radius / 3, + L, + x - radius * 0.5, y - radius, + x + radius * 0.5, y - radius, + x + radius * 0.5, y - radius / 3, + 'Z', + M, // the lower sheet + x - radius * 0.5, y + radius * 0.5, + L, + x - radius * 0.75, y + radius, + x + radius * 0.75, y + radius, + x + radius * 0.5, y + radius * 0.5, + 'Z' + ]; +}; + + +// Add the buttons on chart load +Chart.prototype.callbacks.push(function(chart) { + var n, + exportingOptions = chart.options.exporting, + buttons = exportingOptions.buttons; + + if (exportingOptions.enabled !== false) { + + for (n in buttons) { + chart.addButton(buttons[n]); + } + } +}); + + +})(); \ No newline at end of file diff --git a/js/highcharts/highcharts.js b/js/highcharts/highcharts.js new file mode 100644 index 0000000..c15862b --- /dev/null +++ b/js/highcharts/highcharts.js @@ -0,0 +1,10671 @@ +// ==ClosureCompiler== +// @compilation_level SIMPLE_OPTIMIZATIONS + +/** + * @license Highcharts JS v2.1.4 (2011-03-02) + * + * (c) 2009-2010 Torstein Hønsi + * + * License: www.highcharts.com/license + */ + +// JSLint options: +/*jslint forin: true */ +/*global document, window, navigator, setInterval, clearInterval, clearTimeout, setTimeout, location, jQuery, $ */ + +(function() { +// encapsulated variables +var doc = document, + win = window, + math = Math, + mathRound = math.round, + mathFloor = math.floor, + mathCeil = math.ceil, + mathMax = math.max, + mathMin = math.min, + mathAbs = math.abs, + mathCos = math.cos, + mathSin = math.sin, + mathPI = math.PI, + deg2rad = mathPI * 2 / 360, + + + // some variables + userAgent = navigator.userAgent, + isIE = /msie/i.test(userAgent) && !win.opera, + docMode8 = doc.documentMode == 8, + isWebKit = /AppleWebKit/.test(userAgent), + isFirefox = /Firefox/.test(userAgent), + //hasSVG = win.SVGAngle || doc.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1"), + hasSVG = !!doc.createElementNS && !!doc.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGRect, + SVG_NS = 'http://www.w3.org/2000/svg', + hasTouch = 'ontouchstart' in doc.documentElement, + colorCounter, + symbolCounter, + symbolSizes = {}, + idCounter = 0, + timeFactor = 1, // 1 = JavaScript time, 1000 = Unix time + garbageBin, + defaultOptions, + dateFormat, // function + globalAnimation, + pathAnim, + + + // some constants for frequently used strings + UNDEFINED, + DIV = 'div', + ABSOLUTE = 'absolute', + RELATIVE = 'relative', + HIDDEN = 'hidden', + PREFIX = 'highcharts-', + VISIBLE = 'visible', + PX = 'px', + NONE = 'none', + M = 'M', + L = 'L', + /* + * Empirical lowest possible opacities for TRACKER_FILL + * IE6: 0.002 + * IE7: 0.002 + * IE8: 0.002 + * IE9: 0.00000000001 (unlimited) + * FF: 0.00000000001 (unlimited) + * Chrome: 0.000001 + * Safari: 0.000001 + * Opera: 0.00000000001 (unlimited) + */ + TRACKER_FILL = 'rgba(192,192,192,'+ (hasSVG ? 0.000001 : 0.002) +')', // invisible but clickable + NORMAL_STATE = '', + HOVER_STATE = 'hover', + SELECT_STATE = 'select', + + // time methods, changed based on whether or not UTC is used + makeTime, + getMinutes, + getHours, + getDay, + getDate, + getMonth, + getFullYear, + setMinutes, + setHours, + setDate, + setMonth, + setFullYear, + + // check for a custom HighchartsAdapter defined prior to this file + globalAdapter = win.HighchartsAdapter, + adapter = globalAdapter || {}, + + // Utility functions. If the HighchartsAdapter is not defined, adapter is an empty object + // and all the utility functions will be null. In that case they are populated by the + // default adapters below. + each = adapter.each, + grep = adapter.grep, + map = adapter.map, + merge = adapter.merge, + hyphenate = adapter.hyphenate, + addEvent = adapter.addEvent, + removeEvent = adapter.removeEvent, + fireEvent = adapter.fireEvent, + animate = adapter.animate, + stop = adapter.stop, + + // lookup over the types and the associated classes + seriesTypes = {}, + hoverChart; + +/** + * Extend an object with the members of another + * @param {Object} a The object to be extended + * @param {Object} b The object to add to the first one + */ +function extend(a, b) { + if (!a) { + a = {}; + } + for (var n in b) { + a[n] = b[n]; + } + return a; +} + +/** + * Shortcut for parseInt + * @param {Object} s + */ +function pInt(s, mag) { + return parseInt(s, mag || 10); +} + +/** + * Check for string + * @param {Object} s + */ +function isString(s) { + return typeof s == 'string'; +} + +/** + * Check for object + * @param {Object} obj + */ +function isObject(obj) { + return typeof obj == 'object'; +} + +/** + * Check for number + * @param {Object} n + */ +function isNumber(n) { + return typeof n == 'number'; +} + +/** + * Remove last occurence of an item from an array + * @param {Array} arr + * @param {Mixed} item + */ +function erase(arr, item) { + var i = arr.length; + while (i--) { + if (arr[i] == item) { + arr.splice(i, 1); + break; + } + } + //return arr; +} + +/** + * Returns true if the object is not null or undefined. Like MooTools' $.defined. + * @param {Object} obj + */ +function defined (obj) { + return obj !== UNDEFINED && obj !== null; +} + +/** + * Set or get an attribute or an object of attributes. Can't use jQuery attr because + * it attempts to set expando properties on the SVG element, which is not allowed. + * + * @param {Object} elem The DOM element to receive the attribute(s) + * @param {String|Object} prop The property or an abject of key-value pairs + * @param {String} value The value if a single property is set + */ +function attr(elem, prop, value) { + var key, + setAttribute = 'setAttribute', + ret; + + // if the prop is a string + if (isString(prop)) { + // set the value + if (defined(value)) { + + elem[setAttribute](prop, value); + + // get the value + } else if (elem && elem.getAttribute) { // elem not defined when printing pie demo... + ret = elem.getAttribute(prop); + } + + // else if prop is defined, it is a hash of key/value pairs + } else if (defined(prop) && isObject(prop)) { + for (key in prop) { + elem[setAttribute](key, prop[key]); + } + } + return ret; +} +/** + * Check if an element is an array, and if not, make it into an array. Like + * MooTools' $.splat. + */ +function splat(obj) { + if (!obj || obj.constructor != Array) { + obj = [obj]; + } + return obj; +} + + + +/** + * Return the first value that is defined. Like MooTools' $.pick. + */ +function pick() { + var args = arguments, + i, + arg, + length = args.length; + for (i = 0; i < length; i++) { + arg = args[i]; + if (typeof arg !== 'undefined' && arg !== null) { + return arg; + } + } +} +/** + * Make a style string from a JS object + * @param {Object} style + */ +function serializeCSS(style) { + var s = '', + key; + // serialize the declaration + for (key in style) { + s += hyphenate(key) +':'+ style[key] + ';'; + } + return s; + +} +/** + * Set CSS on a give element + * @param {Object} el + * @param {Object} styles + */ +function css (el, styles) { + if (isIE) { + if (styles && styles.opacity !== UNDEFINED) { + styles.filter = 'alpha(opacity='+ (styles.opacity * 100) +')'; + } + } + extend(el.style, styles); +} + +/** + * Utility function to create element with attributes and styles + * @param {Object} tag + * @param {Object} attribs + * @param {Object} styles + * @param {Object} parent + * @param {Object} nopad + */ +function createElement (tag, attribs, styles, parent, nopad) { + var el = doc.createElement(tag); + if (attribs) { + extend(el, attribs); + } + if (nopad) { + css(el, {padding: 0, border: NONE, margin: 0}); + } + if (styles) { + css(el, styles); + } + if (parent) { + parent.appendChild(el); + } + return el; +} + +/** + * Set the global animation to either a given value, or fall back to the + * given chart's animation option + * @param {Object} animation + * @param {Object} chart + */ +function setAnimation(animation, chart) { + globalAnimation = pick(animation, chart.animation); +} + +/* + * Define the adapter for frameworks. If an external adapter is not defined, + * Highcharts reverts to the built-in jQuery adapter. + */ +if (globalAdapter && globalAdapter.init) { + globalAdapter.init(); +} +if (!globalAdapter && win.jQuery) { + var jQ = jQuery; + + /** + * Utility for iterating over an array. Parameters are reversed compared to jQuery. + * @param {Array} arr + * @param {Function} fn + */ + each = function(arr, fn) { + for (var i = 0, len = arr.length; i < len; i++) { + if (fn.call(arr[i], arr[i], i, arr) === false) { + return i; + } + } + }; + + /** + * Filter an array + */ + grep = jQ.grep; + + /** + * Map an array + * @param {Array} arr + * @param {Function} fn + */ + map = function(arr, fn){ + //return jQuery.map(arr, fn); + var results = []; + for (var i = 0, len = arr.length; i < len; i++) { + results[i] = fn.call(arr[i], arr[i], i, arr); + } + return results; + + }; + + /** + * Deep merge two objects and return a third object + */ + merge = function(){ + var args = arguments; + return jQ.extend(true, null, args[0], args[1], args[2], args[3]); + }; + + /** + * Convert a camelCase string to a hyphenated string + * @param {String} str + */ + hyphenate = function (str) { + return str.replace(/([A-Z])/g, function(a, b){ return '-'+ b.toLowerCase(); }); + }; + + /** + * Add an event listener + * @param {Object} el A HTML element or custom object + * @param {String} event The event type + * @param {Function} fn The event handler + */ + addEvent = function (el, event, fn){ + jQ(el).bind(event, fn); + }; + + /** + * Remove event added with addEvent + * @param {Object} el The object + * @param {String} eventType The event type. Leave blank to remove all events. + * @param {Function} handler The function to remove + */ + removeEvent = function(el, eventType, handler) { + // workaround for jQuery issue with unbinding custom events: + // http://forum.jquery.com/topic/javascript-error-when-unbinding-a-custom-event... + var func = doc.removeEventListener ? 'removeEventListener' : 'detachEvent'; + if (doc[func] && !el[func]) { + el[func] = function() {}; + } + + jQ(el).unbind(eventType, handler); + }; + + /** + * Fire an event on a custom object + * @param {Object} el + * @param {String} type + * @param {Object} eventArguments + * @param {Function} defaultFunction + */ + fireEvent = function(el, type, eventArguments, defaultFunction) { + var event = jQ.Event(type), + detachedType = 'detached'+ type; + extend(event, eventArguments); + + // Prevent jQuery from triggering the object method that is named the + // same as the event. For example, if the event is 'select', jQuery + // attempts calling el.select and it goes into a loop. + if (el[type]) { + el[detachedType] = el[type]; + el[type] = null; + } + + // trigger it + jQ(el).trigger(event); + + // attach the method + if (el[detachedType]) { + el[type] = el[detachedType]; + el[detachedType] = null; + } + + if (defaultFunction && !event.isDefaultPrevented()) { + defaultFunction(event); + } + }; + + /** + * Animate a HTML element or SVG element wrapper + * @param {Object} el + * @param {Object} params + * @param {Object} options jQuery-like animation options: duration, easing, callback + */ + animate = function (el, params, options) { + var $el = jQ(el); + if (params.d) { + el.toD = params.d; // keep the array form for paths, used in jQ.fx.step.d + params.d = 1; // because in jQuery, animating to an array has a different meaning + } + + $el.stop(); + $el.animate(params, options); + + }; + /** + * Stop running animation + */ + stop = function (el) { + jQ(el).stop(); + }; + + + // extend jQuery + jQ.extend( jQ.easing, { + easeOutQuad: function (x, t, b, c, d) { + return -c *(t/=d)*(t-2) + b; + } + }); + + // extend the animate function to allow SVG animations + var oldStepDefault = jQuery.fx.step._default, + oldCur = jQuery.fx.prototype.cur; + + // do the step + jQ.fx.step._default = function(fx){ + var elem = fx.elem; + if (elem.attr) { // is SVG element wrapper + elem.attr(fx.prop, fx.now); + } else { + oldStepDefault.apply(this, arguments); + } + }; + // animate paths + jQ.fx.step.d = function(fx) { + var elem = fx.elem; + + + // Normally start and end should be set in state == 0, but sometimes, + // for reasons unknown, this doesn't happen. Perhaps state == 0 is skipped + // in these cases + if (!fx.started) { + var ends = pathAnim.init(elem, elem.d, elem.toD); + fx.start = ends[0]; + fx.end = ends[1]; + fx.started = true; + } + + + // interpolate each value of the path + elem.attr('d', pathAnim.step(fx.start, fx.end, fx.pos, elem.toD)); + + }; + // get the current value + jQ.fx.prototype.cur = function() { + var elem = this.elem, + r; + if (elem.attr) { // is SVG element wrapper + r = elem.attr(this.prop); + } else { + r = oldCur.apply(this, arguments); + } + return r; + }; +} + + +/** + * Add a global listener for mousemove events + */ +/*addEvent(doc, 'mousemove', function(e) { + if (globalMouseMove) { + globalMouseMove(e); + } +});*/ + +/** + * Path interpolation algorithm used across adapters + */ +pathAnim = { + /** + * Prepare start and end values so that the path can be animated one to one + */ + init: function(elem, fromD, toD) { + fromD = fromD || ''; + var shift = elem.shift, + bezier = fromD.indexOf('C') > -1, + numParams = bezier ? 7 : 3, + endLength, + slice, + i, + start = fromD.split(' '), + end = [].concat(toD), // copy + startBaseLine, + endBaseLine, + sixify = function(arr) { // in splines make move points have six parameters like bezier curves + i = arr.length; + while (i--) { + if (arr[i] == M) { + arr.splice(i + 1, 0, arr[i+1], arr[i+2], arr[i+1], arr[i+2]); + } + } + }; + + if (bezier) { + sixify(start); + sixify(end); + } + + // pull out the base lines before padding + if (elem.isArea) { + startBaseLine = start.splice(start.length - 6, 6); + endBaseLine = end.splice(end.length - 6, 6); + } + + // if shifting points, prepend a dummy point to the end path + if (shift) { + + end = [].concat(end).splice(0, numParams).concat(end); + elem.shift = false; // reset for following animations + } + + // copy and append last point until the length matches the end length + if (start.length) { + endLength = end.length; + while (start.length < endLength) { + + //bezier && sixify(start); + slice = [].concat(start).splice(start.length - numParams, numParams); + if (bezier) { // disable first control point + slice[numParams - 6] = slice[numParams - 2]; + slice[numParams - 5] = slice[numParams - 1]; + } + start = start.concat(slice); + } + } + + if (startBaseLine) { // append the base lines for areas + start = start.concat(startBaseLine); + end = end.concat(endBaseLine); + } + return [start, end]; + }, + + /** + * Interpolate each value of the path and return the array + */ + step: function(start, end, pos, complete) { + var ret = [], + i = start.length, + startVal; + + if (pos == 1) { // land on the final path without adjustment points appended in the ends + ret = complete; + + } else if (i == end.length && pos < 1) { + while (i--) { + startVal = parseFloat(start[i]); + ret[i] = + isNaN(startVal) ? // a letter instruction like M or L + start[i] : + pos * (parseFloat(end[i] - startVal)) + startVal; + + } + } else { // if animation is finished or length not matching, land on right value + ret = end; + } + return ret; + } +}; + +/** + * Set the time methods globally based on the useUTC option. Time method can be either + * local time or UTC (default). + */ +function setTimeMethods() { + var useUTC = defaultOptions.global.useUTC; + + makeTime = useUTC ? Date.UTC : function(year, month, date, hours, minutes, seconds) { + return new Date( + year, + month, + pick(date, 1), + pick(hours, 0), + pick(minutes, 0), + pick(seconds, 0) + ).getTime(); + }; + getMinutes = useUTC ? 'getUTCMinutes' : 'getMinutes'; + getHours = useUTC ? 'getUTCHours' : 'getHours'; + getDay = useUTC ? 'getUTCDay' : 'getDay'; + getDate = useUTC ? 'getUTCDate' : 'getDate'; + getMonth = useUTC ? 'getUTCMonth' : 'getMonth'; + getFullYear = useUTC ? 'getUTCFullYear' : 'getFullYear'; + setMinutes = useUTC ? 'setUTCMinutes' : 'setMinutes'; + setHours = useUTC ? 'setUTCHours' : 'setHours'; + setDate = useUTC ? 'setUTCDate' : 'setDate'; + setMonth = useUTC ? 'setUTCMonth' : 'setMonth'; + setFullYear = useUTC ? 'setUTCFullYear' : 'setFullYear'; + +} + +/** + * Merge the default options with custom options and return the new options structure + * @param {Object} options The new custom options + */ +function setOptions(options) { + defaultOptions = merge(defaultOptions, options); + + // apply UTC + setTimeMethods(); + + return defaultOptions; +} + +/** + * Get the updated default options. Merely exposing defaultOptions for outside modules + * isn't enough because the setOptions method creates a new object. + */ +function getOptions() { + return defaultOptions; +} + +/** + * Discard an element by moving it to the bin and delete + * @param {Object} The HTML node to discard + */ +function discardElement(element) { + // create a garbage bin element, not part of the DOM + if (!garbageBin) { + garbageBin = createElement(DIV); + } + + // move the node and empty bin + if (element) { + garbageBin.appendChild(element); + } + garbageBin.innerHTML = ''; +} + +/* **************************************************************************** + * Handle the options * + *****************************************************************************/ +var + +defaultLabelOptions = { + enabled: true, + // rotation: 0, + align: 'center', + x: 0, + y: 15, + /*formatter: function() { + return this.value; + },*/ + style: { + color: '#666', + fontSize: '11px', + lineHeight: '14px' + } +}; + +defaultOptions = { + colors: ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', + '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'], + symbols: ['circle', 'diamond', 'square', 'triangle', 'triangle-down'], + lang: { + loading: 'Loading...', + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'], + weekdays: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], + decimalPoint: '.', + resetZoom: 'Reset zoom', + resetZoomTitle: 'Reset zoom level 1:1', + thousandsSep: ',' + }, + global: { + useUTC: true + }, + chart: { + //animation: true, + //alignTicks: false, + //reflow: true, + //className: null, + //events: { load, selection }, + //margin: [null], + //marginTop: null, + //marginRight: null, + //marginBottom: null, + //marginLeft: null, + borderColor: '#4572A7', + //borderWidth: 0, + borderRadius: 5, + defaultSeriesType: 'line', + ignoreHiddenSeries: true, + //inverted: false, + //shadow: false, + spacingTop: 10, + spacingRight: 10, + spacingBottom: 15, + spacingLeft: 10, + style: { + fontFamily: '"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif', // default font + fontSize: '12px' + }, + backgroundColor: '#FFFFFF', + //plotBackgroundColor: null, + plotBorderColor: '#C0C0C0' + //plotBorderWidth: 0, + //plotShadow: false, + //zoomType: '' + }, + title: { + text: 'Chart title', + align: 'center', + // floating: false, + // margin: 15, + // x: 0, + // verticalAlign: 'top', + y: 15, // docs + style: { + color: '#3E576F', + fontSize: '16px' + } + + }, + subtitle: { + text: '', + align: 'center', + // floating: false + // x: 0, + // verticalAlign: 'top', + y: 30, // docs + style: { + color: '#6D869F' + } + }, + + plotOptions: { + line: { // base series options + allowPointSelect: false, + showCheckbox: false, + animation: { + duration: 1000 + }, + //cursor: 'default', + //dashStyle: null, + //enableMouseTracking: true, + events: {}, + lineWidth: 2, + shadow: true, + // stacking: null, + marker: { + enabled: true, + //symbol: null, + lineWidth: 0, + radius: 4, + lineColor: '#FFFFFF', + //fillColor: null, + states: { // states for a single point + hover: { + //radius: base + 2 + }, + select: { + fillColor: '#FFFFFF', + lineColor: '#000000', + lineWidth: 2 + } + } + }, + point: { + events: {} + }, + dataLabels: merge(defaultLabelOptions, { + enabled: false, + y: -6, + formatter: function() { + return this.y; + } + }), + + //pointStart: 0, + //pointInterval: 1, + showInLegend: true, + states: { // states for the entire series + hover: { + //enabled: false, + //lineWidth: base + 1, + marker: { + // lineWidth: base + 1, + // radius: base + 1 + } + }, + select: { + marker: {} + } + }, + stickyTracking: true + //zIndex: null + } + }, + labels: { + //items: [], + style: { + //font: defaultFont, + position: ABSOLUTE, + color: '#3E576F' + } + }, + legend: { + enabled: true, + align: 'center', + //floating: false, + layout: 'horizontal', + labelFormatter: function() { + return this.name; + }, + // lineHeight: 16, // docs: deprecated + borderWidth: 1, + borderColor: '#909090', + borderRadius: 5, + // margin: 10, + // reversed: false, + shadow: false, + // backgroundColor: null, + style: { + padding: '5px' + }, + itemStyle: { + cursor: 'pointer', + color: '#3E576F' + }, + itemHoverStyle: { + cursor: 'pointer', + color: '#000000' + }, + itemHiddenStyle: { + color: '#C0C0C0' + }, + itemCheckboxStyle: { + position: ABSOLUTE, + width: '13px', // for IE precision + height: '13px' + }, + // itemWidth: undefined, + symbolWidth: 16, + symbolPadding: 5, + verticalAlign: 'bottom', + // width: undefined, + x: 0, // docs + y: 0 // docs + }, + + loading: { + hideDuration: 100, + labelStyle: { + fontWeight: 'bold', + position: RELATIVE, + top: '1em' + }, + showDuration: 100, + style: { + position: ABSOLUTE, + backgroundColor: 'white', + opacity: 0.5, + textAlign: 'center' + } + }, + + tooltip: { + enabled: true, + //crosshairs: null, + backgroundColor: 'rgba(255, 255, 255, .85)', + borderWidth: 2, + borderRadius: 5, + //formatter: defaultFormatter, + shadow: true, + //shared: false, + snap: hasTouch ? 25 : 10, + style: { + color: '#333333', + fontSize: '12px', + padding: '5px', + whiteSpace: 'nowrap' + } + }, + + toolbar: { + itemStyle: { + color: '#4572A7', + cursor: 'pointer' + } + }, + + credits: { + enabled: true, + text: 'Highcharts.com', + href: 'http://www.highcharts.com', + position: { + align: 'right', + x: -10, + verticalAlign: 'bottom', + y: -5 + }, + style: { + cursor: 'pointer', + color: '#909090', + fontSize: '10px' + } + } +}; + +// Axis defaults +var defaultXAxisOptions = { + // allowDecimals: null, + // alternateGridColor: null, + // categories: [], + dateTimeLabelFormats: { + second: '%H:%M:%S', + minute: '%H:%M', + hour: '%H:%M', + day: '%e. %b', + week: '%e. %b', + month: '%b '%y', + year: '%Y' + }, + endOnTick: false, + gridLineColor: '#C0C0C0', + // gridLineDashStyle: 'solid', // docs + // gridLineWidth: 0, + // reversed: false, + + labels: defaultLabelOptions, + // { step: null }, + lineColor: '#C0D0E0', + lineWidth: 1, + //linkedTo: null, + max: null, + min: null, + minPadding: 0.01, + maxPadding: 0.01, + //maxZoom: null, + minorGridLineColor: '#E0E0E0', + // minorGridLineDashStyle: null, + minorGridLineWidth: 1, + minorTickColor: '#A0A0A0', + //minorTickInterval: null, + minorTickLength: 2, + minorTickPosition: 'outside', // inside or outside + //minorTickWidth: 0, + //opposite: false, + //offset: 0, + //plotBands: [{ + // events: {}, + // zIndex: 1, + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //plotLines: [{ + // events: {} + // dashStyle: {} + // zIndex: + // labels: { align, x, verticalAlign, y, style, rotation, textAlign } + //}], + //reversed: false, + // showFirstLabel: true, + // showLastLabel: false, + startOfWeek: 1, + startOnTick: false, + tickColor: '#C0D0E0', + //tickInterval: null, + tickLength: 5, + tickmarkPlacement: 'between', // on or between + tickPixelInterval: 100, + tickPosition: 'outside', + tickWidth: 1, + title: { + //text: null, + align: 'middle', // low, middle or high + //margin: 0 for horizontal, 10 for vertical axes, + //rotation: 0, + //side: 'outside', + style: { + color: '#6D869F', + //font: defaultFont.replace('normal', 'bold') + fontWeight: 'bold' + } + //x: 0, + //y: 0 + }, + type: 'linear' // linear or datetime +}, + +defaultYAxisOptions = merge(defaultXAxisOptions, { + endOnTick: true, + gridLineWidth: 1, + tickPixelInterval: 72, + showLastLabel: true, + labels: { + align: 'right', + x: -8, + y: 3 + }, + lineWidth: 0, + maxPadding: 0.05, + minPadding: 0.05, + startOnTick: true, + tickWidth: 0, + title: { + rotation: 270, + text: 'Y-values' + } +}), + +defaultLeftAxisOptions = { + labels: { + align: 'right', + x: -8, + y: null // docs + }, + title: { + rotation: 270 + } +}, +defaultRightAxisOptions = { + labels: { + align: 'left', + x: 8, + y: null // docs + }, + title: { + rotation: 90 + } +}, +defaultBottomAxisOptions = { // horizontal axis + labels: { + align: 'center', + x: 0, + y: 14 + // staggerLines: null + }, + title: { + rotation: 0 + } +}, +defaultTopAxisOptions = merge(defaultBottomAxisOptions, { + labels: { + y: -5 + // staggerLines: null + } +}); + + + + +// Series defaults +var defaultPlotOptions = defaultOptions.plotOptions, + defaultSeriesOptions = defaultPlotOptions.line; +//defaultPlotOptions.line = merge(defaultSeriesOptions); +defaultPlotOptions.spline = merge(defaultSeriesOptions); +defaultPlotOptions.scatter = merge(defaultSeriesOptions, { + lineWidth: 0, + states: { + hover: { + lineWidth: 0 + } + } +}); +defaultPlotOptions.area = merge(defaultSeriesOptions, { + // threshold: 0, + // lineColor: null, // overrides color, but lets fillColor be unaltered + // fillOpacity: 0.75, + // fillColor: null + +}); +defaultPlotOptions.areaspline = merge(defaultPlotOptions.area); +defaultPlotOptions.column = merge(defaultSeriesOptions, { + borderColor: '#FFFFFF', + borderWidth: 1, + borderRadius: 0, + //colorByPoint: undefined, + groupPadding: 0.2, + marker: null, // point options are specified in the base options + pointPadding: 0.1, + //pointWidth: null, + minPointLength: 0, + states: { + hover: { + brightness: 0.1, + shadow: false + }, + select: { + color: '#C0C0C0', + borderColor: '#000000', + shadow: false + } + } +}); +defaultPlotOptions.bar = merge(defaultPlotOptions.column, { + dataLabels: { + align: 'left', + x: 5, + y: 0 + } +}); +defaultPlotOptions.pie = merge(defaultSeriesOptions, { + //dragType: '', // n/a + borderColor: '#FFFFFF', + borderWidth: 1, + center: ['50%', '50%'], + colorByPoint: true, // always true for pies + dataLabels: { + // align: null, + // connectorWidth: 1, + // connectorColor: '#606060', + // connectorPadding: 5, + distance: 30, + enabled: true, + formatter: function() { + return this.point.name; + }, + y: 5 + }, + //innerSize: 0, + legendType: 'point', + marker: null, // point options are specified in the base options + size: '75%', + showInLegend: false, + slicedOffset: 10, + states: { + hover: { + brightness: 0.1, + shadow: false + } + } + +}); + +// set the default time methods +setTimeMethods(); + + +/** + * Extend a prototyped class by new members + * @param {Object} parent + * @param {Object} members + */ +function extendClass(parent, members) { + var object = function(){}; + object.prototype = new parent(); + extend(object.prototype, members); + return object; +} + + +/** + * Handle color operations. The object methods are chainable. + * @param {String} input The input color in either rbga or hex format + */ +var Color = function(input) { + // declare variables + var rgba = [], result; + + /** + * Parse the input color to rgba array + * @param {String} input + */ + function init(input) { + + // rgba + if((result = /rgba(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]?(?:.[0-9]+)?)\s*)/.exec(input))) { + rgba = [pInt(result[1]), pInt(result[2]), pInt(result[3]), parseFloat(result[4], 10)]; + } + + // hex + else if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(input))) { + rgba = [pInt(result[1],16), pInt(result[2],16), pInt(result[3],16), 1]; + } + + } + /** + * Return the color a specified format + * @param {String} format + */ + function get(format) { + var ret; + + // it's NaN if gradient colors on a column chart + if (rgba && !isNaN(rgba[0])) { + if (format == 'rgb') { + ret = 'rgb('+ rgba[0] +','+ rgba[1] +','+ rgba[2] +')'; + } else if (format == 'a') { + ret = rgba[3]; + } else { + ret = 'rgba('+ rgba.join(',') +')'; + } + } else { + ret = input; + } + return ret; + } + + /** + * Brighten the color + * @param {Number} alpha + */ + function brighten(alpha) { + if (isNumber(alpha) && alpha !== 0) { + var i; + for (i = 0; i < 3; i++) { + rgba[i] += pInt(alpha * 255); + + if (rgba[i] < 0) { + rgba[i] = 0; + } + if (rgba[i] > 255) { + rgba[i] = 255; + } + } + } + return this; + } + /** + * Set the color's opacity to a given alpha value + * @param {Number} alpha + */ + function setOpacity(alpha) { + rgba[3] = alpha; + return this; + } + + // initialize: parse the input + init(input); + + // public methods + return { + get: get, + brighten: brighten, + setOpacity: setOpacity + }; +}; + + + +/** + * Format a number and return a string based on input settings + * @param {Number} number The input number to format + * @param {Number} decimals The amount of decimals + * @param {String} decPoint The decimal point, defaults to the one given in the lang options + * @param {String} thousandsSep The thousands separator, defaults to the one given in the lang options + */ +function numberFormat (number, decimals, decPoint, thousandsSep) { + var lang = defaultOptions.lang, + // http://kevin.vanzonneveld.net/techblog/article/javascript_equivalent_for_php... + n = number, c = isNaN(decimals = mathAbs(decimals)) ? 2 : decimals, + d = decPoint === undefined ? lang.decimalPoint : decPoint, + t = thousandsSep === undefined ? lang.thousandsSep : thousandsSep, s = n < 0 ? "-" : "", + i = pInt(n = mathAbs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0; + + return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + + (c ? d + mathAbs(n - i).toFixed(c).slice(2) : ""); +} + +/** + * Based on http://www.php.net/manual/en/function.strftime.php + * @param {String} format + * @param {Number} timestamp + * @param {Boolean} capitalize + */ +dateFormat = function (format, timestamp, capitalize) { + function pad (number) { + return number.toString().replace(/^([0-9])$/, '0$1'); + } + + if (!defined(timestamp) || isNaN(timestamp)) { + return 'Invalid date'; + } + format = pick(format, '%Y-%m-%d %H:%M:%S'); + + var date = new Date(timestamp * timeFactor), + + // get the basic time values + hours = date[getHours](), + day = date[getDay](), + dayOfMonth = date[getDate](), + month = date[getMonth](), + fullYear = date[getFullYear](), + lang = defaultOptions.lang, + langWeekdays = lang.weekdays, + langMonths = lang.months, + + // list all format keys + replacements = { + + // Day + 'a': langWeekdays[day].substr(0, 3), // Short weekday, like 'Mon' + 'A': langWeekdays[day], // Long weekday, like 'Monday' + 'd': pad(dayOfMonth), // Two digit day of the month, 01 to 31 + 'e': dayOfMonth, // Day of the month, 1 through 31 + + // Week (none implemented) + + // Month + 'b': langMonths[month].substr(0, 3), // Short month, like 'Jan' + 'B': langMonths[month], // Long month, like 'January' + 'm': pad(month + 1), // Two digit month number, 01 through 12 + + // Year + 'y': fullYear.toString().substr(2, 2), // Two digits year, like 09 for 2009 + 'Y': fullYear, // Four digits year, like 2009 + + // Time + 'H': pad(hours), // Two digits hours in 24h format, 00 through 23 + 'I': pad((hours % 12) || 12), // Two digits hours in 12h format, 00 through 11 + 'l': (hours % 12) || 12, // Hours in 12h format, 1 through 12 + 'M': pad(date[getMinutes]()), // Two digits minutes, 00 through 59 + 'p': hours < 12 ? 'AM' : 'PM', // Upper case AM or PM + 'P': hours < 12 ? 'am' : 'pm', // Lower case AM or PM + 'S': pad(date.getSeconds()) // Two digits seconds, 00 through 59 + + }; + + + // do the replaces + for (var key in replacements) { + format = format.replace('%'+ key, replacements[key]); + } + + // Optionally capitalize the string and return + return capitalize ? format.substr(0, 1).toUpperCase() + format.substr(1) : format; +}; + + + +/** + * Loop up the node tree and add offsetWidth and offsetHeight to get the + * total page offset for a given element. Used by Opera and iOS on hover and + * all browsers on point click. + * + * @param {Object} el + * + */ +function getPosition (el) { + var p = { left: el.offsetLeft, top: el.offsetTop }; + while ((el = el.offsetParent)) { + p.left += el.offsetLeft; + p.top += el.offsetTop; + if (el != doc.body && el != doc.documentElement) { + p.left -= el.scrollLeft; + p.top -= el.scrollTop; + } + } + return p; +} + + +/** + * A wrapper object for SVG elements + */ +function SVGElement () {} + +SVGElement.prototype = { + /** + * Initialize the SVG renderer + * @param {Object} renderer + * @param {String} nodeName + */ + init: function(renderer, nodeName) { + this.element = doc.createElementNS(SVG_NS, nodeName); + this.renderer = renderer; + }, + /** + * Animate a given attribute + * @param {Object} params + * @param {Number} options The same options as in jQuery animation + * @param {Function} complete Function to perform at the end of animation + */ + animate: function(params, options, complete) { + var animOptions = pick(options, globalAnimation, true); + if (animOptions) { + animOptions = merge(animOptions); + if (complete) { // allows using a callback with the global animation without overwriting it + animOptions.complete = complete; + } + animate(this, params, animOptions); + } else { + this.attr(params); + if (complete) { + complete(); + } + } + }, + /** + * Set or get a given attribute + * @param {Object|String} hash + * @param {Mixed|Undefined} val + */ + attr: function(hash, val) { + var key, + value, + i, + child, + element = this.element, + nodeName = element.nodeName, + renderer = this.renderer, + skipAttr, + shadows = this.shadows, + hasSetSymbolSize, + ret = this; + + // single key-value pair + if (isString(hash) && defined(val)) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter: first argument is a string, second is undefined + if (isString(hash)) { + key = hash; + if (nodeName == 'circle') { + key = { x: 'cx', y: 'cy' }[key] || key; + } else if (key == 'strokeWidth') { + key = 'stroke-width'; + } + ret = attr(element, key) || this[key] || 0; + + if (key != 'd' && key != 'visibility') { // 'd' is string in animation step + ret = parseFloat(ret); + } + + // setter + } else { + + for (key in hash) { + skipAttr = false; // reset + value = hash[key]; + + // paths + if (key == 'd') { + if (value && value.join) { // join path + value = value.join(' '); + } + if (/(NaN| {2}|^$)/.test(value)) { + value = 'M 0 0'; + } + this.d = value; // shortcut for animations + + // update child tspans x values + } else if (key == 'x' && nodeName == 'text') { + for (i = 0; i < element.childNodes.length; i++ ) { + child = element.childNodes[i]; + // if the x values are equal, the tspan represents a linebreak + if (attr(child, 'x') == attr(element, 'x')) { + //child.setAttribute('x', value); + attr(child, 'x', value); + } + } + + if (this.rotation) { + attr(element, 'transform', 'rotate('+ this.rotation +' '+ value +' '+ + pInt(hash.y || attr(element, 'y')) +')'); + } + + // apply gradients + } else if (key == 'fill') { + value = renderer.color(value, element, key); + + // circle x and y + } else if (nodeName == 'circle' && (key == 'x' || key == 'y')) { + key = { x: 'cx', y: 'cy' }[key] || key; + + // translation and text rotation + } else if (key == 'translateX' || key == 'translateY' || key == 'rotation' || key == 'verticalAlign') { + this[key] = value; + this.updateTransform(); + skipAttr = true; + + // apply opacity as subnode (required by legacy WebKit and Batik) + } else if (key == 'stroke') { + value = renderer.color(value, element, key); + + // emulate VML's dashstyle implementation + } else if (key == 'dashstyle') { + key = 'stroke-dasharray'; + if (value) { + value = value.toLowerCase() + .replace('shortdashdotdot', '3,1,1,1,1,1,') + .replace('shortdashdot', '3,1,1,1') + .replace('shortdot', '1,1,') + .replace('shortdash', '3,1,') + .replace('longdash', '8,3,') + .replace(/dot/g, '1,3,') + .replace('dash', '4,3,') + .replace(/,$/, '') + .split(','); // ending comma + + i = value.length; + while (i--) { + value[i] = pInt(value[i]) * hash['stroke-width']; + } + value = value.join(','); + } + + // special + } else if (key == 'isTracker') { + this[key] = value; + + // IE9/MooTools combo: MooTools returns objects instead of numbers and IE9 Beta 2 + // is unable to cast them. Test again with final IE9. + } else if (key == 'width') { + value = pInt(value); + + // Text alignment + } else if (key == 'align') { + key = 'text-anchor'; + value = { left: 'start', center: 'middle', right: 'end' }[value]; + } + + + + // jQuery animate changes case + if (key == 'strokeWidth') { + key = 'stroke-width'; + } + + // Chrome/Win < 6 bug (http://code.google.com/p/chromium/issues/detail?id=15461) + if (isWebKit && key == 'stroke-width' && value === 0) { + value = 0.000001; + } + + // symbols + if (this.symbolName && /^(x|y|r|start|end|innerR)/.test(key)) { + + + if (!hasSetSymbolSize) { + this.symbolAttr(hash); + hasSetSymbolSize = true; + } + skipAttr = true; + } + + // let the shadow follow the main element + if (shadows && /^(width|height|visibility|x|y|d)$/.test(key)) { + i = shadows.length; + while (i--) { + attr(shadows[i], key, value); + } + } + + /* trows errors in Chrome + if ((key == 'width' || key == 'height') && nodeName == 'rect' && value < 0) { + console.log(element); + } + */ + + + + if (key == 'text') { + // only one node allowed + this.textStr = value; + if (this.added) { + renderer.buildText(this); + } + } else if (!skipAttr) { + //element.setAttribute(key, value); + attr(element, key, value); + } + + } + + } + return ret; + }, + + /** + * If one of the symbol size affecting parameters are changed, + * check all the others only once for each call to an element's + * .attr() method + * @param {Object} hash + */ + symbolAttr: function(hash) { + var wrapper = this; + + each (['x', 'y', 'r', 'start', 'end', 'width', 'height', 'innerR'], function(key) { + wrapper[key] = pick(hash[key], wrapper[key]); + }); + + wrapper.attr({ + d: wrapper.renderer.symbols[wrapper.symbolName](wrapper.x, wrapper.y, wrapper.r, { + start: wrapper.start, + end: wrapper.end, + width: wrapper.width, + height: wrapper.height, + innerR: wrapper.innerR + }) + }); + }, + + /** + * Apply a clipping path to this object + * @param {String} id + */ + clip: function(clipRect) { + return this.attr('clip-path', 'url('+ this.renderer.url +'#'+ clipRect.id +')'); + }, + + /** + * Calculate the coordinates needed for drawing a rectangle crisply and return the + * calculated attributes + * @param {Number} strokeWidth + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + crisp: function(strokeWidth, x, y, width, height) { + + var wrapper = this, + key, + attr = {}, + values = {}, + normalizer; + + strokeWidth = strokeWidth || wrapper.strokeWidth || 0; + normalizer = strokeWidth % 2 / 2; + + // normalize for crisp edges + values.x = mathFloor(x || wrapper.x || 0) + normalizer; + values.y = mathFloor(y || wrapper.y || 0) + normalizer; + values.width = mathFloor((width || wrapper.width || 0) - 2 * normalizer); + values.height = mathFloor((height || wrapper.height || 0) - 2 * normalizer); + values.strokeWidth = strokeWidth; + + for (key in values) { + if (wrapper[key] != values[key]) { // only set attribute if changed + wrapper[key] = attr[key] = values[key]; + } + } + + return attr; + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: function(styles) { + var elemWrapper = this, + elem = elemWrapper.element, + textWidth = styles && styles.width && elem.nodeName == 'text'; + + // convert legacy + if (styles && styles.color) { + styles.fill = styles.color; + } + + // save the styles in an object + styles = extend( + elemWrapper.styles, + styles + ); + + + // store object + elemWrapper.styles = styles; + + // serialize and set style attribute + if (isIE && !hasSVG) { // legacy IE doesn't support setting style attribute + if (textWidth) { + delete styles.width; + } + css(elemWrapper.element, styles); + } else { + elemWrapper.attr({ + style: serializeCSS(styles) + }); + } + + + // re-build text + if (textWidth && elemWrapper.added) { + elemWrapper.renderer.buildText(elemWrapper); + } + + return elemWrapper; + }, + + /** + * Add an event listener + * @param {String} eventType + * @param {Function} handler + */ + on: function(eventType, handler) { + var fn = handler; + // touch + if (hasTouch && eventType == 'click') { + eventType = 'touchstart'; + fn = function(e) { + e.preventDefault(); + handler(); + } + } + // simplest possible event model for internal use + this.element['on'+ eventType] = fn; + return this; + }, + + + /** + * Move an object and its children by x and y values + * @param {Number} x + * @param {Number} y + */ + translate: function(x, y) { + return this.attr({ + translateX: x, + translateY: y + }); + }, + + /** + * Invert a group, rotate and flip + */ + invert: function() { + var wrapper = this; + wrapper.inverted = true; + wrapper.updateTransform(); + return wrapper; + }, + + /** + * Private method to update the transform attribute based on internal + * properties + */ + updateTransform: function() { + var wrapper = this, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + inverted = wrapper.inverted, + rotation = wrapper.rotation, + transform = []; + + // flipping affects translate as adjustment for flipping around the group's axis + if (inverted) { + translateX += wrapper.attr('width'); + translateY += wrapper.attr('height'); + } + + // apply translate + if (translateX || translateY) { + transform.push('translate('+ translateX +','+ translateY +')'); + } + + // apply rotation + if (inverted) { + transform.push('rotate(90) scale(-1,1)'); + } else if (rotation) { // text rotation + transform.push('rotate('+ rotation +' '+ wrapper.x +' '+ wrapper.y +')'); + } + + if (transform.length) { + attr(wrapper.element, 'transform', transform.join(' ')); + } + }, + /** + * Bring the element to the front + */ + toFront: function() { + var element = this.element; + element.parentNode.appendChild(element); + return this; + }, + + + /** + * Break down alignment options like align, verticalAlign, x and y + * to x and y relative to the chart. + * + * @param {Object} alignOptions + * @param {Boolean} alignByTranslate + * @param {Object} box The box to align to, needs a width and height + * + */ + align: function(alignOptions, alignByTranslate, box) { + + if (!alignOptions) { // called on resize + alignOptions = this.alignOptions; + alignByTranslate = this.alignByTranslate; + } else { // first call on instanciate + this.alignOptions = alignOptions; + this.alignByTranslate = alignByTranslate; + if (!box) { // boxes other than renderer handle this internally + this.renderer.alignedObjects.push(this); + } + } + + box = pick(box, this.renderer); + + var align = alignOptions.align, + vAlign = alignOptions.verticalAlign, + x = (box.x || 0) + (alignOptions.x || 0), // default: left align + y = (box.y || 0) + (alignOptions.y || 0), // default: top align + attribs = {}; + + + // align + if (/^(right|center)$/.test(align)) { + x += (box.width - (alignOptions.width || 0) ) / + { right: 1, center: 2 }[align]; + } + attribs[alignByTranslate ? 'translateX' : 'x'] = mathRound(x); + + + // vertical align + if (/^(bottom|middle)$/.test(vAlign)) { + y += (box.height - (alignOptions.height || 0)) / + ({ bottom: 1, middle: 2 }[vAlign] || 1); + + } + attribs[alignByTranslate ? 'translateY' : 'y'] = mathRound(y); + + // animate only if already placed + this[this.placed ? 'animate' : 'attr'](attribs); + this.placed = true; + + return this; + }, + + /** + * Get the bounding box (width, height, x and y) for the element + */ + getBBox: function() { + var bBox, + width, + height, + rotation = this.rotation, + rad = rotation * deg2rad; + + try { // fails in Firefox if the container has display: none + // use extend because IE9 is not allowed to change width and height in case + // of rotation (below) + bBox = extend({}, this.element.getBBox()); + } catch(e) { + bBox = { width: 0, height: 0 }; + } + width = bBox.width; + height = bBox.height; + + // adjust for rotated text + if (rotation) { + bBox.width = mathAbs(height * mathSin(rad)) + mathAbs(width * mathCos(rad)); + bBox.height = mathAbs(height * mathCos(rad)) + mathAbs(width * mathSin(rad)); + } + + return bBox; + }, + + /* * + * Manually compute width and height of rotated text from non-rotated. Shared by SVG and VML + * @param {Object} bBox + * @param {number} rotation + * / + rotateBBox: function(bBox, rotation) { + var rad = rotation * math.PI * 2 / 360, // radians + width = bBox.width, + height = bBox.height; + + + },*/ + + /** + * Show the element + */ + show: function() { + return this.attr({ visibility: VISIBLE }); + }, + + /** + * Hide the element + */ + hide: function() { + return this.attr({ visibility: HIDDEN }); + }, + + /** + * Add the element + * @param {Object|Undefined} parent Can be an element, an element wrapper or undefined + * to append the element to the renderer.box. + */ + add: function(parent) { + + var renderer = this.renderer, + parentWrapper = parent || renderer, + parentNode = parentWrapper.element || renderer.box, + childNodes = parentNode.childNodes, + element = this.element, + zIndex = attr(element, 'zIndex'), + otherElement, + otherZIndex, + i; + + // mark as inverted + this.parentInverted = parent && parent.inverted; + + // build formatted text + if (this.textStr !== undefined) { + renderer.buildText(this); + } + + // mark the container as having z indexed children + if (zIndex) { + parentWrapper.handleZ = true; + zIndex = pInt(zIndex); + } + + // insert according to this and other elements' zIndex + if (parentWrapper.handleZ) { // this element or any of its siblings has a z index + for (i = 0; i < childNodes.length; i++) { + otherElement = childNodes[i]; + otherZIndex = attr(otherElement, 'zIndex'); + if (otherElement != element && ( + // insert before the first element with a higher zIndex + pInt(otherZIndex) > zIndex || + // if no zIndex given, insert before the first element with a zIndex + (!defined(zIndex) && defined(otherZIndex)) + + )) { + parentNode.insertBefore(element, otherElement); + return this; + } + } + } + + // default: append at the end + parentNode.appendChild(element); + + this.added = true; + + return this; + }, + + /** + * Destroy the element and element wrapper + */ + destroy: function() { + var wrapper = this, + element = wrapper.element || {}, + shadows = wrapper.shadows, + parentNode = element.parentNode, + key; + + // remove events + element.onclick = element.onmouseout = element.onmouseover = element.onmousemove = null; + stop(wrapper); // stop running animations + + // remove element + if (parentNode) { + parentNode.removeChild(element); + } + + // destroy shadows + if (shadows) { + each(shadows, function(shadow) { + parentNode = shadow.parentNode; + if (parentNode) { // the entire chart HTML can be overwritten + parentNode.removeChild(shadow); + } + }); + } + + // remove from alignObjects + erase(wrapper.renderer.alignedObjects, wrapper); + + for (key in wrapper) { + delete wrapper[key]; + } + + return null; + }, + + /** + * Empty a group element + */ + empty: function() { + var element = this.element, + childNodes = element.childNodes, + i = childNodes.length; + + while (i--) { + element.removeChild(childNodes[i]); + } + }, + + /** + * Add a shadow to the element. Must be done after the element is added to the DOM + * @param {Boolean} apply + */ + shadow: function(apply) { + var shadows = [], + i, + shadow, + element = this.element, + + // compensate for inverted plot area + transform = this.parentInverted ? '(-1,-1)' : '(1,1)'; + + + if (apply) { + for (i = 1; i <= 3; i++) { + shadow = element.cloneNode(0); + attr(shadow, { + 'isShadow': 'true', + 'stroke': 'rgb(0, 0, 0)', + 'stroke-opacity': 0.05 * i, + 'stroke-width': 7 - 2 * i, + 'transform': 'translate'+ transform, + 'fill': NONE + }); + + + element.parentNode.insertBefore(shadow, element); + + shadows.push(shadow); + } + + this.shadows = shadows; + } + return this; + + } +}; + + + +/** + * The default SVG renderer + */ +var SVGRenderer = function() { + this.init.apply(this, arguments); +}; +SVGRenderer.prototype = { + /** + * Initialize the SVGRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + * @param {Boolean} forExport + */ + init: function(container, width, height, forExport) { + var renderer = this, + loc = location, + boxWrapper; + + renderer.Element = SVGElement; + boxWrapper = renderer.createElement('svg') + .attr({ + xmlns: SVG_NS, + version: '1.1' + }); + container.appendChild(boxWrapper.element); + + // object properties + renderer.box = boxWrapper.element; + renderer.boxWrapper = boxWrapper; + renderer.alignedObjects = []; + renderer.url = isIE ? '' : loc.href.replace(/#.*?$/, ''); // page url used for internal references + renderer.defs = this.createElement('defs').add(); + renderer.forExport = forExport; + + renderer.setSize(width, height, false); + + }, + + + /** + * Create a wrapper for an SVG element + * @param {Object} nodeName + */ + createElement: function(nodeName) { + var wrapper = new this.Element(); + wrapper.init(this, nodeName); + return wrapper; + }, + + + /** + * Parse a simple HTML string into SVG tspans + * + * @param {Object} textNode The parent text SVG node + */ + buildText: function(wrapper) { + var textNode = wrapper.element, + lines = pick(wrapper.textStr, '').toString() + .replace(/<(b|strong)>/g, '<span style="font-weight:bold">') + .replace(/<(i|em)>/g, '<span style="font-style:italic">') + .replace(/<a/g, '<span') + .replace(/</(b|strong|i|em|a)>/g, '</span>') + .split(/<br[^>]?>/g), + childNodes = textNode.childNodes, + styleRegex = /style="([^"]+)"/, + hrefRegex = /href="([^"]+)"/, + parentX = attr(textNode, 'x'), + textStyles = wrapper.styles, + reverse = isFirefox && textStyles && textStyles.HcDirection == 'rtl' && !this.forExport, // issue #38 + arr, + width = textStyles && pInt(textStyles.width), + textLineHeight = textStyles && textStyles.lineHeight, + lastLine, + i = childNodes.length; + + // remove old text + while (i--) { + textNode.removeChild(childNodes[i]); + } + + if (width && !wrapper.added) { + this.box.appendChild(textNode); // attach it to the DOM to read offset width + } + + each(lines, function(line, lineNo) { + var spans, spanNo = 0, lineHeight; + + line = line.replace(/<span/g, '|||<span').replace(/</span>/g, '</span>|||'); + spans = line.split('|||'); + + each(spans, function (span) { + if (span !== '' || spans.length == 1) { + var attributes = {}, + tspan = doc.createElementNS(SVG_NS, 'tspan'); + if (styleRegex.test(span)) { + attr( + tspan, + 'style', + span.match(styleRegex)[1].replace(/(;| |^)color([ :])/, '$1fill$2') + ); + } + if (hrefRegex.test(span)) { + attr(tspan, 'onclick', 'location.href="'+ span.match(hrefRegex)[1] +'"'); + css(tspan, { cursor: 'pointer' }); + } + + span = span.replace(/<(.|\n)*?>/g, '') || ' '; + + // issue #38 workaround. + if (reverse) { + arr = []; + i = span.length; + while (i--) { + arr.push(span.charAt(i)) + } + span = arr.join(''); + } + + // add the text node + tspan.appendChild(doc.createTextNode(span)); + + if (!spanNo) { // first span in a line, align it to the left + attributes.x = parentX; + } else { + // Firefox ignores spaces at the front or end of the tspan + attributes.dx = 3; // space + } + + // first span on subsequent line, add the line height + if (!spanNo) { + if (lineNo) { + // Webkit and opera sometimes return 'normal' as the line height. In that + // case, webkit uses offsetHeight, while Opera falls back to 18 + lineHeight = pInt(window.getComputedStyle(lastLine, null).getPropertyValue('line-height')); + if (isNaN(lineHeight)) { + lineHeight = textLineHeight || lastLine.offsetHeight || 18; + } + attr(tspan, 'dy', lineHeight); + } + lastLine = tspan; // record for use in next line + } + + // add attributes + attr(tspan, attributes); + + // append it + textNode.appendChild(tspan); + + spanNo++; + + // check width and apply soft breaks + if (width) { + var words = span.replace(/-/g, '- ').split(' '), + tooLong, + actualWidth, + rest = []; + + while (words.length || rest.length) { + actualWidth = textNode.getBBox().width; + tooLong = actualWidth > width; + if (!tooLong || words.length == 1) { // new line needed + words = rest; + rest = []; + if (words.length) { + tspan = doc.createElementNS(SVG_NS, 'tspan'); + attr(tspan, { + x: parentX, + dy: textLineHeight || 16 + }); + textNode.appendChild(tspan); + + if (actualWidth > width) { // a single word is pressing it out + width = actualWidth; + } + } + } else { // append to existing line tspan + tspan.removeChild(tspan.firstChild); + rest.unshift(words.pop()); + } + + tspan.appendChild(doc.createTextNode(words.join(' ').replace(/- /g, '-'))); + } + + } + } + }); + }); + + + }, + + /** + * Make a straight line crisper by not spilling out to neighbour pixels + * @param {Array} points + * @param {Number} width + */ + crispLine: function(points, width) { + // points format: [M, 0, 0, L, 100, 0] + // normalize to a crisp line + if (points[1] == points[4]) { + points[1] = points[4] = mathRound(points[1]) + (width % 2 / 2); + } + if (points[2] == points[5]) { + points[2] = points[5] = mathRound(points[2]) + (width % 2 / 2); + } + return points; + }, + + + /** + * Draw a path + * @param {Array} path An SVG path in array form + */ + path: function (path) { + return this.createElement('path').attr({ + d: path, + fill: NONE + }); + }, + + /** + * Draw and return an SVG circle + * @param {Number} x The x position + * @param {Number} y The y position + * @param {Number} r The radius + */ + circle: function (x, y, r) { + var attr = isObject(x) ? + x : + { + x: x, + y: y, + r: r + }; + + return this.createElement('circle').attr(attr); + }, + + /** + * Draw and return an arc + * @param {Number} x X position + * @param {Number} y Y position + * @param {Number} r Radius + * @param {Number} innerR Inner radius like used in donut charts + * @param {Number} start Starting angle + * @param {Number} end Ending angle + */ + arc: function (x, y, r, innerR, start, end) { + // arcs are defined as symbols for the ability to set + // attributes in attr and animate + + if (isObject(x)) { + y = x.y; + r = x.r; + innerR = x.innerR; + start = x.start; + end = x.end; + x = x.x; + } + + return this.symbol('arc', x || 0, y || 0, r || 0, { + innerR: innerR || 0, + start: start || 0, + end: end || 0 + }); + }, + + /** + * Draw and return a rectangle + * @param {Number} x Left position + * @param {Number} y Top position + * @param {Number} width + * @param {Number} height + * @param {Number} r Border corner radius + * @param {Number} strokeWidth A stroke width can be supplied to allow crisp drawing + */ + rect: function (x, y, width, height, r, strokeWidth) { + if (isObject(x)) { + y = x.y; + width = x.width; + height = x.height; + r = x.r; + x = x.x; + } + var wrapper = this.createElement('rect').attr({ + rx: r, + ry: r, + fill: NONE + }); + + return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); + }, + + /** + * Resize the box and re-align all aligned elements + * @param {Object} width + * @param {Object} height + * @param {Boolean} animate + * + */ + setSize: function(width, height, animate) { + var renderer = this, + alignedObjects = renderer.alignedObjects, + i = alignedObjects.length; + + renderer.width = width; + renderer.height = height; + + renderer.boxWrapper[pick(animate, true) ? 'animate' : 'attr']({ + width: width, + height: height + }); + + while (i--) { + alignedObjects[i].align(); + } + }, + + /** + * Create a group + * @param {String} name The group will be given a class name of 'highcharts-{name}'. + * This can be used for styling and scripting. + */ + g: function(name) { + return this.createElement('g').attr( + defined(name) && { 'class': PREFIX + name } + ); + }, + + /** + * Display an image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function(src, x, y, width, height) { + var attribs = { + preserveAspectRatio: NONE + }, + elemWrapper; + + // optional properties + if (arguments.length > 1) { + extend(attribs, { + x: x, + y: y, + width: width, + height: height + }); + } + + elemWrapper = this.createElement('image').attr(attribs); + + // set the href in the xlink namespace + elemWrapper.element.setAttributeNS('http://www.w3.org/1999/xlink', + 'href', src); + + return elemWrapper; + }, + + /** + * Draw a symbol out of pre-defined shape paths from the namespace 'symbol' object. + * + * @param {Object} symbol + * @param {Object} x + * @param {Object} y + * @param {Object} radius + * @param {Object} options + */ + symbol: function(symbol, x, y, radius, options) { + + var obj, + + // get the symbol definition function + symbolFn = this.symbols[symbol], + + // check if there's a path defined for this symbol + path = symbolFn && symbolFn( + x, + y, + radius, + options + ), + + imageRegex = /^url((.*?))$/, + imageSrc; + + if (path) { + + obj = this.path(path); + // expando properties for use in animate and attr + extend(obj, { + symbolName: symbol, + x: x, + y: y, + r: radius + }); + if (options) { + extend(obj, options); + } + + + // image symbols + } else if (imageRegex.test(symbol)) { + + imageSrc = symbol.match(imageRegex)[1]; + + // create the image synchronously, add attribs async + obj = this.image(imageSrc) + .attr({ + x: x, + y: y + }); + + // create a dummy JavaScript image to get the width and height + createElement('img', { + onload: function() { + var img = this, + size = symbolSizes[img.src] || [img.width, img.height]; + obj.attr({ + width: size[0], + height: size[1] + }).translate( + -mathRound(size[0] / 2), + -mathRound(size[1] / 2) + ); + }, + src: imageSrc + }); + + // default circles + } else { + obj = this.circle(x, y, radius); + } + + return obj; + }, + + /** + * An extendable collection of functions for defining symbol paths. + */ + symbols: { + 'square': function (x, y, radius) { + var len = 0.707 * radius; + return [ + M, x-len, y-len, + L, x+len, y-len, + x+len, y+len, + x-len, y+len, + 'Z' + ]; + }, + + 'triangle': function (x, y, radius) { + return [ + M, x, y-1.33 * radius, + L, x+radius, y + 0.67 * radius, + x-radius, y + 0.67 * radius, + 'Z' + ]; + }, + + 'triangle-down': function (x, y, radius) { + return [ + M, x, y + 1.33 * radius, + L, x-radius, y-0.67 * radius, + x+radius, y-0.67 * radius, + 'Z' + ]; + }, + 'diamond': function (x, y, radius) { + return [ + M, x, y-radius, + L, x+radius, y, + x, y+radius, + x-radius, y, + 'Z' + ]; + }, + 'arc': function (x, y, radius, options) { + var start = options.start, + end = options.end - 0.000001, // to prevent cos and sin of start and end from becoming equal on 360 arcs + innerRadius = options.innerR, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + longArc = options.end - start < mathPI ? 0 : 1; + + return [ + M, + x + radius * cosStart, + y + radius * sinStart, + 'A', // arcTo + radius, // x radius + radius, // y radius + 0, // slanting + longArc, // long or short arc + 1, // clockwise + x + radius * cosEnd, + y + radius * sinEnd, + L, + x + innerRadius * cosEnd, + y + innerRadius * sinEnd, + 'A', // arcTo + innerRadius, // x radius + innerRadius, // y radius + 0, // slanting + longArc, // long or short arc + 0, // clockwise + x + innerRadius * cosStart, + y + innerRadius * sinStart, + + 'Z' // close + ]; + } + }, + + /** + * Define a clipping rectangle + * @param {String} id + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + var wrapper, + id = PREFIX + idCounter++, + + clipPath = this.createElement('clipPath').attr({ + id: id + }).add(this.defs); + + wrapper = this.rect(x, y, width, height, 0).add(clipPath); + wrapper.id = id; + + return wrapper; + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object + * + * @param {Object} color The color or config object + */ + color: function(color, elem, prop) { + var colorObject, + regexRgba = /^rgba/; + if (color && color.linearGradient) { + var renderer = this, + strLinearGradient = 'linearGradient', + linearGradient = color[strLinearGradient], + id = PREFIX + idCounter++, + gradientObject, + stopColor, + stopOpacity; + gradientObject = renderer.createElement(strLinearGradient).attr({ + id: id, + gradientUnits: 'userSpaceOnUse', + x1: linearGradient[0], + y1: linearGradient[1], + x2: linearGradient[2], + y2: linearGradient[3] + }).add(renderer.defs); + + each(color.stops, function(stop) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + renderer.createElement('stop').attr({ + offset: stop[0], + 'stop-color': stopColor, + 'stop-opacity': stopOpacity + }).add(gradientObject); + }); + + return 'url('+ this.url +'#'+ id +')'; + + // Webkit and Batik can't show rgba. + } else if (regexRgba.test(color)) { + colorObject = Color(color); + attr(elem, prop +'-opacity', colorObject.get('a')); + + return colorObject.get('rgb'); + + + } else { + return color; + } + + }, + + + /** + * Add text to the SVG object + * @param {String} str + * @param {Number} x Left position + * @param {Number} y Top position + */ + text: function(str, x, y) { + + // declare variables + var defaultChartStyle = defaultOptions.chart.style, + wrapper; + + x = mathRound(pick(x, 0)); + y = mathRound(pick(y, 0)); + + wrapper = this.createElement('text') + .attr({ + x: x, + y: y, + text: str + }) + .css({ + 'font-family': defaultChartStyle.fontFamily, + 'font-size': defaultChartStyle.fontSize + }); + + wrapper.x = x; + wrapper.y = y; + return wrapper; + } +}; // end SVGRenderer + + + + +/* **************************************************************************** + * * + * START OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + * For applications and websites that don't need IE support, like platform * + * targeted mobile apps and web apps, this code can be removed. * + * * + *****************************************************************************/ +var VMLRenderer; +if (!hasSVG) { + +/** + * The VML element wrapper. + */ +var VMLElement = extendClass( SVGElement, { + + /** + * Initialize a new VML element wrapper. It builds the markup as a string + * to minimize DOM traffic. + * @param {Object} renderer + * @param {Object} nodeName + */ + init: function(renderer, nodeName) { + var markup = ['<', nodeName, ' filled="f" stroked="f"'], + style = ['position: ', ABSOLUTE, ';']; + + // divs and shapes need size + if (nodeName == 'shape' || nodeName == DIV) { + style.push('left:0;top:0;width:10px;height:10px;'); + } + if (docMode8) { + style.push('visibility: ', nodeName == DIV ? HIDDEN : VISIBLE); + } + + markup.push(' style="', style.join(''), '"/>'); + + // create element with default attributes and style + if (nodeName) { + markup = nodeName == DIV || nodeName == 'span' || nodeName == 'img' ? + markup.join('') + : renderer.prepVML(markup); + this.element = createElement(markup); + } + + this.renderer = renderer; + }, + + /** + * Add the node to the given parent + * @param {Object} parent + */ + add: function(parent) { + var wrapper = this, + renderer = wrapper.renderer, + element = wrapper.element, + box = renderer.box, + inverted = parent && parent.inverted, + + // get the parent node + parentNode = parent ? + parent.element || parent : + box; + + + // if the parent group is inverted, apply inversion on all children + if (inverted) { // only on groups + renderer.invertChild(element, parentNode); + } + + // issue #140 workaround - related to #61 and #74 + if (docMode8 && parentNode.gVis == HIDDEN) { + css(element, { visibility: HIDDEN }); + } + + // append it + parentNode.appendChild(element); + + // align text after adding to be able to read offset + wrapper.added = true; + if (wrapper.alignOnAdd) { + wrapper.updateTransform(); + } + + return wrapper; + }, + + /** + * Get or set attributes + */ + attr: function(hash, val) { + var key, + value, + i, + element = this.element || {}, + elemStyle = element.style, + nodeName = element.nodeName, + renderer = this.renderer, + symbolName = this.symbolName, + childNodes, + hasSetSymbolSize, + shadows = this.shadows, + skipAttr, + ret = this; + + // single key-value pair + if (isString(hash) && defined(val)) { + key = hash; + hash = {}; + hash[key] = val; + } + + // used as a getter, val is undefined + if (isString(hash)) { + key = hash; + if (key == 'strokeWidth' || key == 'stroke-width') { + ret = this.strokeweight; + } else { + ret = this[key]; + } + + // setter + } else { + for (key in hash) { + value = hash[key]; + skipAttr = false; + + // prepare paths + // symbols + if (symbolName && /^(x|y|r|start|end|width|height|innerR)/.test(key)) { + // if one of the symbol size affecting parameters are changed, + // check all the others only once for each call to an element's + // .attr() method + if (!hasSetSymbolSize) { + + this.symbolAttr(hash); + + hasSetSymbolSize = true; + } + + skipAttr = true; + + } else if (key == 'd') { + value = value || []; + this.d = value.join(' '); // used in getter for animation + + // convert paths + i = value.length; + var convertedPath = []; + while (i--) { + + // Multiply by 10 to allow subpixel precision. + // Substracting half a pixel seems to make the coordinates + // align with SVG, but this hasn't been tested thoroughly + if (isNumber(value[i])) { + convertedPath[i] = mathRound(value[i] * 10) - 5; + } + // close the path + else if (value[i] == 'Z') { + convertedPath[i] = 'x'; + } + else { + convertedPath[i] = value[i]; + } + + } + value = convertedPath.join(' ') || 'x'; + element.path = value; + + // update shadows + if (shadows) { + i = shadows.length; + while (i--) { + shadows[i].path = value; + } + } + skipAttr = true; + + // directly mapped to css + } else if (key == 'zIndex' || key == 'visibility') { + + // issue 61 workaround + if (docMode8 && key == 'visibility' && nodeName == 'DIV') { + element.gVis = value; + childNodes = element.childNodes; + i = childNodes.length; + while (i--) { + css(childNodes[i], { visibility: value }); + } + if (value == VISIBLE) { // issue 74 + value = null; + } + } + + if (value) { + elemStyle[key] = value; + } + + + + skipAttr = true; + + // width and height + } else if (/^(width|height)$/.test(key)) { + + + // clipping rectangle special + if (this.updateClipping) { + this[key] = value; + this.updateClipping(); + + } else { + // normal + elemStyle[key] = value; + } + + skipAttr = true; + + // x and y + } else if (/^(x|y)$/.test(key)) { + + this[key] = value; // used in getter + + if (element.tagName == 'SPAN') { + this.updateTransform(); + + } else { + elemStyle[{ x: 'left', y: 'top' }[key]] = value; + } + + // class name + } else if (key == 'class') { + // IE8 Standards mode has problems retrieving the className + element.className = value; + + // stroke + } else if (key == 'stroke') { + + value = renderer.color(value, element, key); + + key = 'strokecolor'; + + // stroke width + } else if (key == 'stroke-width' || key == 'strokeWidth') { + element.stroked = value ? true : false; + key = 'strokeweight'; + this[key] = value; // used in getter, issue #113 + if (isNumber(value)) { + value += PX; + } + + // dashStyle + } else if (key == 'dashstyle') { + var strokeElem = element.getElementsByTagName('stroke')[0] || + createElement(renderer.prepVML(['<stroke/>']), null, null, element); + strokeElem[key] = value || 'solid'; + this.dashstyle = value; /* because changing stroke-width will change the dash length + and cause an epileptic effect */ + skipAttr = true; + + // fill + } else if (key == 'fill') { + + if (nodeName == 'SPAN') { // text color + elemStyle.color = value; + } else { + element.filled = value != NONE ? true : false; + + value = renderer.color(value, element, key); + + key = 'fillcolor'; + } + + // translation for animation + } else if (key == 'translateX' || key == 'translateY' || key == 'rotation' || key == 'align') { + if (key == 'align') { + key = 'textAlign'; + } + this[key] = value; + this.updateTransform(); + + skipAttr = true; + } + + // text for rotated and non-rotated elements + else if (key == 'text') { + element.innerHTML = value; + skipAttr = true; + } + + + // let the shadow follow the main element + if (shadows && key == 'visibility') { + i = shadows.length; + while (i--) { + shadows[i].style[key] = value; + } + } + + + + if (!skipAttr) { + if (docMode8) { // IE8 setAttribute bug + element[key] = value; + } else { + attr(element, key, value); + } + } + } + } + return ret; + }, + + /** + * Set the element's clipping to a predefined rectangle + * + * @param {String} id The id of the clip rectangle + */ + clip: function(clipRect) { + var wrapper = this, + clipMembers = clipRect.members; + + clipMembers.push(wrapper); + wrapper.destroyClip = function() { + erase(clipMembers, wrapper); + }; + return wrapper.css(clipRect.getCSS(wrapper.inverted)); + }, + + /** + * Set styles for the element + * @param {Object} styles + */ + css: function(styles) { + var wrapper = this, + element = wrapper.element, + textWidth = styles && element.tagName == 'SPAN' && styles.width; + + /*if (textWidth) { + extend(styles, { + display: 'block', + whiteSpace: 'normal' + }); + }*/ + if (textWidth) { + delete styles.width; + wrapper.textWidth = textWidth; + wrapper.updateTransform(); + } + + wrapper.styles = extend(wrapper.styles, styles); + css(wrapper.element, styles); + + + + return wrapper; + }, + + /** + * Extend element.destroy by removing it from the clip members array + */ + destroy: function() { + var wrapper = this; + + if (wrapper.destroyClip) { + wrapper.destroyClip(); + } + + SVGElement.prototype.destroy.apply(wrapper); + }, + + /** + * Remove all child nodes of a group, except the v:group element + */ + empty: function() { + var element = this.element, + childNodes = element.childNodes, + i = childNodes.length, + node; + + while (i--) { + node = childNodes[i]; + node.parentNode.removeChild(node); + } + }, + + /** + * VML override for calculating the bounding box based on offsets + * + * @return {Object} A hash containing values for x, y, width and height + */ + + getBBox: function() { + var element = this.element; + + // faking getBBox in exported SVG in legacy IE + if (element.nodeName == 'text') { + element.style.position = ABSOLUTE; + } + + return { + x: element.offsetLeft, + y: element.offsetTop, + width: element.offsetWidth, + height: element.offsetHeight + }; + + }, + + /** + * Add an event listener. VML override for normalizing event parameters. + * @param {String} eventType + * @param {Function} handler + */ + on: function(eventType, handler) { + // simplest possible event model for internal use + this.element['on'+ eventType] = function() { + var evt = win.event; + evt.target = evt.srcElement; + handler(evt); + }; + return this; + }, + + + /** + * VML override private method to update elements based on internal + * properties based on SVG transform + */ + updateTransform: function(hash) { + // aligning non added elements is expensive + if (!this.added) { + this.alignOnAdd = true; + return; + } + + var wrapper = this, + elem = wrapper.element, + translateX = wrapper.translateX || 0, + translateY = wrapper.translateY || 0, + x = wrapper.x || 0, + y = wrapper.y || 0, + align = wrapper.textAlign || 'left', + alignCorrection = { left: 0, center: 0.5, right: 1 }[align], + nonLeft = align && align != 'left'; + + // apply translate + if (translateX || translateY) { + wrapper.css({ + marginLeft: translateX, + marginTop: translateY + }); + } + + // apply inversion + if (wrapper.inverted) { // wrapper is a group + each(elem.childNodes, function(child) { + wrapper.renderer.invertChild(child, elem); + }); + } + + if (elem.tagName == 'SPAN') { + + var width, height, + rotation = wrapper.rotation, + lineHeight, + radians = 0, + costheta = 1, + sintheta = 0, + quad, + textWidth = pInt(wrapper.textWidth), + xCorr = wrapper.xCorr || 0, + yCorr = wrapper.yCorr || 0, + currentTextTransform = [rotation, align, elem.innerHTML, wrapper.textWidth].join(','); + + if (currentTextTransform != wrapper.cTT) { // do the calculations and DOM access only if properties changed + + if (defined(rotation)) { + radians = rotation * deg2rad; // deg to rad + costheta = mathCos(radians); + sintheta = mathSin(radians); + + // Adjust for alignment and rotation. + // Test case: http://highcharts.com/tests/?file=text-rotation + css(elem, { + filter: rotation ? ['progid:DXImageTransform.Microsoft.Matrix(M11=', costheta, + ', M12=', -sintheta, ', M21=', sintheta, ', M22=', costheta, + ', sizingMethod='auto expand')'].join('') : NONE + }); + } + + width = elem.offsetWidth; + height = elem.offsetHeight; + + // update textWidth + if (width > textWidth) { + css(elem, { + width: textWidth +PX, + display: 'block', + whiteSpace: 'normal' + }); + width = textWidth; + } + + // correct x and y + lineHeight = mathRound(pInt(elem.style.fontSize || 12) * 1.2); + xCorr = costheta < 0 && -width; + yCorr = sintheta < 0 && -height; + + // correct for lineHeight and corners spilling out after rotation + quad = costheta * sintheta < 0; + xCorr += sintheta * lineHeight * (quad ? 1 - alignCorrection : alignCorrection); + yCorr -= costheta * lineHeight * (rotation ? (quad ? alignCorrection : 1 - alignCorrection) : 1); + + // correct for the length/height of the text + if (nonLeft) { + xCorr -= width * alignCorrection * (costheta < 0 ? -1 : 1); + if (rotation) { + yCorr -= height * alignCorrection * (sintheta < 0 ? -1 : 1); + } + css(elem, { + textAlign: align + }); + } + + // record correction + wrapper.xCorr = xCorr; + wrapper.yCorr = yCorr; + } + + // apply position with correction + css(elem, { + left: x + xCorr, + top: y + yCorr + }); + + // record current text transform + wrapper.cTT = currentTextTransform; + } + }, + + /** + * Apply a drop shadow by copying elements and giving them different strokes + * @param {Boolean} apply + */ + shadow: function(apply) { + var shadows = [], + i, + element = this.element, + renderer = this.renderer, + shadow, + elemStyle = element.style, + markup, + path = element.path; + + // the path is some mysterious string-like object that can be cast to a string + if (''+ element.path === '') { + path = 'x'; + } + + if (apply) { + for (i = 1; i <= 3; i++) { + markup = ['<shape isShadow="true" strokeweight="', ( 7 - 2 * i ) , + '" filled="false" path="', path, + '" coordsize="100,100" style="', element.style.cssText, '" />']; + shadow = createElement(renderer.prepVML(markup), + null, { + left: pInt(elemStyle.left) + 1, + top: pInt(elemStyle.top) + 1 + } + ); + + // apply the opacity + markup = ['<stroke color="black" opacity="', (0.05 * i), '"/>']; + createElement(renderer.prepVML(markup), null, null, shadow); + + + // insert it + element.parentNode.insertBefore(shadow, element); + + // record it + shadows.push(shadow); + + } + + this.shadows = shadows; + } + return this; + + } +}); + +/** + * The VML renderer + */ +VMLRenderer = function() { + this.init.apply(this, arguments); +}; +VMLRenderer.prototype = merge( SVGRenderer.prototype, { // inherit SVGRenderer + + isIE8: userAgent.indexOf('MSIE 8.0') > -1, + + + /** + * Initialize the VMLRenderer + * @param {Object} container + * @param {Number} width + * @param {Number} height + */ + init: function(container, width, height) { + var renderer = this, + boxWrapper; + + renderer.Element = VMLElement; + renderer.alignedObjects = []; + + boxWrapper = renderer.createElement(DIV); + container.appendChild(boxWrapper.element); + + + // generate the containing box + renderer.box = boxWrapper.element; + renderer.boxWrapper = boxWrapper; + + + renderer.setSize(width, height, false); + + // The only way to make IE6 and IE7 print is to use a global namespace. However, + // with IE8 the only way to make the dynamic shapes visible in screen and print mode + // seems to be to add the xmlns attribute and the behaviour style inline. + if (!doc.namespaces.hcv) { + + doc.namespaces.add('hcv', 'urn:schemas-microsoft-com:vml'); + + // setup default css + doc.createStyleSheet().cssText = + 'hcv\:fill, hcv\:path, hcv\:shape, hcv\:stroke'+ + '{ behavior:url(#default#VML); display: inline-block; } '; + + } + }, + + /** + * Define a clipping rectangle. In VML it is accomplished by storing the values + * for setting the CSS style to all associated members. + * + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + clipRect: function (x, y, width, height) { + + // create a dummy element + var clipRect = this.createElement(); + + // mimic a rectangle with its style object for automatic updating in attr + return extend(clipRect, { + members: [], + left: x, + top: y, + width: width, + height: height, + getCSS: function(inverted) { + var rect = this,//clipRect.element.style, + top = rect.top, + left = rect.left, + right = left + rect.width, + bottom = top + rect.height, + ret = { + clip: 'rect('+ + mathRound(inverted ? left : top) + 'px,'+ + mathRound(inverted ? bottom : right) + 'px,'+ + mathRound(inverted ? right : bottom) + 'px,'+ + mathRound(inverted ? top : left) +'px)' + }; + + // issue 74 workaround + if (!inverted && docMode8) { + extend(ret, { + width: right +PX, + height: bottom +PX + }); + } + return ret; + }, + + // used in attr and animation to update the clipping of all members + updateClipping: function() { + each(clipRect.members, function(member) { + member.css(clipRect.getCSS(member.inverted)); + }); + } + }); + + }, + + + /** + * Take a color and return it if it's a string, make it a gradient if it's a + * gradient configuration object, and apply opacity. + * + * @param {Object} color The color or config object + */ + color: function(color, elem, prop) { + var colorObject, + regexRgba = /^rgba/, + markup; + + if (color && color.linearGradient) { + + var stopColor, + stopOpacity, + linearGradient = color.linearGradient, + angle, + color1, + opacity1, + color2, + opacity2; + + each(color.stops, function(stop, i) { + if (regexRgba.test(stop[1])) { + colorObject = Color(stop[1]); + stopColor = colorObject.get('rgb'); + stopOpacity = colorObject.get('a'); + } else { + stopColor = stop[1]; + stopOpacity = 1; + } + + if (!i) { // first + color1 = stopColor; + opacity1 = stopOpacity; + } else { + color2 = stopColor; + opacity2 = stopOpacity; + } + }); + + + + // calculate the angle based on the linear vector + angle = 90 - math.atan( + (linearGradient[3] - linearGradient[1]) / // y vector + (linearGradient[2] - linearGradient[0]) // x vector + ) * 180 / mathPI; + + // when colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + markup = ['<', prop, ' colors="0% ', color1, ',100% ', color2, '" angle="', angle, + '" opacity="', opacity2, '" o:opacity2="', opacity1, + '" type="gradient" focus="100%" />']; + createElement(this.prepVML(markup), null, null, elem); + + + + // if the color is an rgba color, split it and add a fill node + // to hold the opacity component + } else if (regexRgba.test(color) && elem.tagName != 'IMG') { + + colorObject = Color(color); + + markup = ['<', prop, ' opacity="', colorObject.get('a'), '"/>']; + createElement(this.prepVML(markup), null, null, elem); + + return colorObject.get('rgb'); + + + } else { + return color; + } + + }, + + /** + * Take a VML string and prepare it for either IE8 or IE6/IE7. + * @param {Array} markup A string array of the VML markup to prepare + */ + prepVML: function(markup) { + var vmlStyle = 'display:inline-block;behavior:url(#default#VML);', + isIE8 = this.isIE8; + + markup = markup.join(''); + + if (isIE8) { // add xmlns and style inline + markup = markup.replace('/>', ' xmlns="urn:schemas-microsoft-com:vml" />'); + if (markup.indexOf('style="') == -1) { + markup = markup.replace('/>', ' style="'+ vmlStyle +'" />'); + } else { + markup = markup.replace('style="', 'style="'+ vmlStyle); + } + + } else { // add namespace + markup = markup.replace('<', '<hcv:'); + } + + return markup; + }, + + /** + * Create rotated and aligned text + * @param {String} str + * @param {Number} x + * @param {Number} y + */ + text: function(str, x, y) { + + var defaultChartStyle = defaultOptions.chart.style; + + return this.createElement('span') + .attr({ + text: str, + x: mathRound(x), + y: mathRound(y) + }) + .css({ + whiteSpace: 'nowrap', + fontFamily: defaultChartStyle.fontFamily, + fontSize: defaultChartStyle.fontSize + }); + }, + + /** + * Create and return a path element + * @param {Array} path + */ + path: function (path) { + // create the shape + return this.createElement('shape').attr({ + // subpixel precision down to 0.1 (width and height = 10px) + coordsize: '100 100', + d: path + }); + }, + + /** + * Create and return a circle element. In VML circles are implemented as + * shapes, which is faster than v:oval + * @param {Number} x + * @param {Number} y + * @param {Number} r + */ + circle: function(x, y, r) { + return this.path(this.symbols.circle(x, y, r)); + }, + + /** + * Create a group using an outer div and an inner v:group to allow rotating + * and flipping. A simple v:group would have problems with positioning + * child HTML elements and CSS clip. + * + * @param {String} name The name of the group + */ + g: function(name) { + var wrapper, + attribs; + + // set the class name + if (name) { + attribs = { 'className': PREFIX + name, 'class': PREFIX + name }; + } + + // the div to hold HTML and clipping + wrapper = this.createElement(DIV).attr(attribs); + + return wrapper; + }, + + /** + * VML override to create a regular HTML image + * @param {String} src + * @param {Number} x + * @param {Number} y + * @param {Number} width + * @param {Number} height + */ + image: function(src, x, y, width, height) { + var obj = this.createElement('img') + .attr({ src: src }); + + if (arguments.length > 1) { + obj.css({ + left: x, + top: y, + width: width, + height: height + }); + } + return obj; + }, + + /** + * VML uses a shape for rect to overcome bugs and rotation problems + */ + rect: function(x, y, width, height, r, strokeWidth) { + + if (isObject(x)) { + y = x.y; + width = x.width; + height = x.height; + r = x.r; + x = x.x; + } + var wrapper = this.symbol('rect'); + wrapper.r = r; + + return wrapper.attr(wrapper.crisp(strokeWidth, x, y, mathMax(width, 0), mathMax(height, 0))); + }, + + /** + * In the VML renderer, each child of an inverted div (group) is inverted + * @param {Object} element + * @param {Object} parentNode + */ + invertChild: function(element, parentNode) { + var parentStyle = parentNode.style; + + css(element, { + flip: 'x', + left: pInt(parentStyle.width) - 10, + top: pInt(parentStyle.height) - 10, + rotation: -90 + }); + }, + + /** + * Symbol definitions that override the parent SVG renderer's symbols + * + */ + symbols: { + // VML specific arc function + arc: function (x, y, radius, options) { + var start = options.start, + end = options.end, + cosStart = mathCos(start), + sinStart = mathSin(start), + cosEnd = mathCos(end), + sinEnd = mathSin(end), + innerRadius = options.innerR, + circleCorrection = 0.07 / radius, + innerCorrection = innerRadius && 0.1 / innerRadius || 0; + + if (end - start === 0) { // no angle, don't show it. + return ['x']; + + //} else if (end - start == 2 * mathPI) { // full circle + } else if (2 * mathPI - end + start < circleCorrection) { // full circle + // empirical correction found by trying out the limits for different radii + cosEnd = - circleCorrection; + } else if (end - start < innerCorrection) { // issue #186, another mysterious VML arc problem + cosEnd = mathCos(start + innerCorrection); + } + + return [ + 'wa', // clockwise arc to + x - radius, // left + y - radius, // top + x + radius, // right + y + radius, // bottom + x + radius * cosStart, // start x + y + radius * sinStart, // start y + x + radius * cosEnd, // end x + y + radius * sinEnd, // end y + + + 'at', // anti clockwise arc to + x - innerRadius, // left + y - innerRadius, // top + x + innerRadius, // right + y + innerRadius, // bottom + x + innerRadius * cosEnd, // start x + y + innerRadius * sinEnd, // start y + x + innerRadius * cosStart, // end x + y + innerRadius * sinStart, // end y + + 'x', // finish path + 'e' // close + ]; + + }, + // Add circle symbol path. This performs significantly faster than v:oval. + circle: function (x, y, r) { + return [ + 'wa', // clockwisearcto + x - r, // left + y - r, // top + x + r, // right + y + r, // bottom + x + r, // start x + y, // start y + x + r, // end x + y, // end y + //'x', // finish path + 'e' // close + ]; + }, + /** + * Add rectangle symbol path which eases rotation and omits arcsize problems + * compared to the built-in VML roundrect shape + * + * @param {Number} left Left position + * @param {Number} top Top position + * @param {Number} r Border radius + * @param {Object} options Width and height + */ + + rect: function (left, top, r, options) { + if (!defined(options)) { + return []; + } + var width = options.width, + height = options.height, + right = left + width, + bottom = top + height; + + r = mathMin(r, width, height); + + return [ + M, + left + r, top, + + L, + right - r, top, + 'wa', + right - 2 * r, top, + right, top + 2 * r, + right - r, top, + right, top + r, + + L, + right, bottom - r, + 'wa', + right - 2 * r, bottom - 2 * r, + right, bottom, + right, bottom - r, + right - r, bottom, + + L, + left + r, bottom, + 'wa', + left, bottom - 2 * r, + left + 2 * r, bottom, + left + r, bottom, + left, bottom - r, + + L, + left, top + r, + 'wa', + left, top, + left + 2 * r, top + 2 * r, + left, top + r, + left + r, top, + + + 'x', + 'e' + ]; + + } + } +}); +} +/* **************************************************************************** + * * + * END OF INTERNET EXPLORER <= 8 SPECIFIC CODE * + * * + *****************************************************************************/ + +/** + * General renderer + */ +var Renderer = hasSVG ? SVGRenderer : VMLRenderer; + + +/** + * The chart class + * @param {Object} options + * @param {Function} callback Function to run when the chart has loaded + */ +function Chart (options, callback) { + + defaultXAxisOptions = merge(defaultXAxisOptions, defaultOptions.xAxis); + defaultYAxisOptions = merge(defaultYAxisOptions, defaultOptions.yAxis); + defaultOptions.xAxis = defaultOptions.yAxis = null; + + // Handle regular options + options = merge(defaultOptions, options); + + // Define chart variables + var optionsChart = options.chart, + optionsMargin = optionsChart.margin, + margin = isObject(optionsMargin) ? + optionsMargin : + [optionsMargin, optionsMargin, optionsMargin, optionsMargin], + optionsMarginTop = pick(optionsChart.marginTop, margin[0]), + optionsMarginRight = pick(optionsChart.marginRight, margin[1]), + optionsMarginBottom = pick(optionsChart.marginBottom, margin[2]), + optionsMarginLeft = pick(optionsChart.marginLeft, margin[3]), + spacingTop = optionsChart.spacingTop, + spacingRight = optionsChart.spacingRight, + spacingBottom = optionsChart.spacingBottom, + spacingLeft = optionsChart.spacingLeft, + spacingBox, + chartTitleOptions, + chartSubtitleOptions, + plotTop, + marginRight, + marginBottom, + plotLeft, + axisOffset, + renderTo, + renderToClone, + container, + containerId, + containerWidth, + containerHeight, + chartWidth, + chartHeight, + oldChartWidth, + oldChartHeight, + chartBackground, + plotBackground, + plotBGImage, + plotBorder, + chart = this, + chartEvents = optionsChart.events, + runChartClick = chartEvents && !!chartEvents.click, + eventType, + isInsidePlot, // function + tooltip, + mouseIsDown, + loadingDiv, + loadingSpan, + loadingShown, + plotHeight, + plotWidth, + tracker, + trackerGroup, + placeTrackerGroup, + legend, + legendWidth, + legendHeight, + chartPosition,// = getPosition(container), + hasCartesianSeries = optionsChart.showAxes, + isResizing = 0, + axes = [], + maxTicks, // handle the greatest amount of ticks on grouped axes + series = [], + inverted, + renderer, + tooltipTick, + tooltipInterval, + hoverX, + drawChartBox, // function + getMargins, // function + resetMargins, // function + setChartSize, // function + resize, + zoom, // function + zoomOut; // function + + + /** + * Create a new axis object + * @param {Object} chart + * @param {Object} options + */ + function Axis (chart, options) { + + // Define variables + var isXAxis = options.isX, + opposite = options.opposite, // needed in setOptions + horiz = inverted ? !isXAxis : isXAxis, + side = horiz ? + (opposite ? 0 /* top */ : 2 /* bottom */) : + (opposite ? 1 /* right*/ : 3 /* left */ ), + stacks = {}; + + + options = merge( + isXAxis ? defaultXAxisOptions : defaultYAxisOptions, + [defaultTopAxisOptions, defaultRightAxisOptions, + defaultBottomAxisOptions, defaultLeftAxisOptions][side], + options + ); + + var axis = this, + isDatetimeAxis = options.type == 'datetime', + offset = options.offset || 0, + xOrY = isXAxis ? 'x' : 'y', + axisLength, + transA, // translation factor + oldTransA, // used for prerendering + transB = horiz ? plotLeft : marginBottom, // translation addend + translate, // fn + getPlotLinePath, // fn + axisGroup, + gridGroup, + axisLine, + dataMin, + dataMax, + associatedSeries, + userSetMin, + userSetMax, + max = null, + min = null, + oldMin, + oldMax, + minPadding = options.minPadding, + maxPadding = options.maxPadding, + isLinked = defined(options.linkedTo), + ignoreMinPadding, // can be set to true by a column or bar series + ignoreMaxPadding, + usePercentage, + events = options.events, + eventType, + plotLinesAndBands = [], + tickInterval, + minorTickInterval, + magnitude, + tickPositions, // array containing predefined positions + ticks = {}, + minorTicks = {}, + alternateBands = {}, + tickAmount, + labelOffset, + axisTitleMargin,// = options.title.margin, + dateTimeLabelFormat, + categories = options.categories, + labelFormatter = options.labels.formatter || // can be overwritten by dynamic format + function() { + var value = this.value, + ret; + + if (dateTimeLabelFormat) { // datetime axis + ret = dateFormat(dateTimeLabelFormat, value); + + } else if (tickInterval % 1000000 === 0) { // use M abbreviation + ret = (value / 1000000) +'M'; + + } else if (tickInterval % 1000 === 0) { // use k abbreviation + ret = (value / 1000) +'k'; + + } else if (!categories && value >= 1000) { // add thousands separators + ret = numberFormat(value, 0); + + } else { // strings (categories) and small numbers + ret = value; + } + return ret; + }, + + staggerLines = horiz && options.labels.staggerLines, + reversed = options.reversed, + tickmarkOffset = (categories && options.tickmarkPlacement == 'between') ? 0.5 : 0; + + /** + * The Tick class + */ + function Tick(pos, minor) { + var tick = this; + tick.pos = pos; + tick.minor = minor; + tick.isNew = true; + + if (!minor) { + tick.addLabel(); + } + } + Tick.prototype = { + /** + * Write the tick label + */ + addLabel: function() { + var pos = this.pos, + labelOptions = options.labels, + str, + withLabel = !((pos == min && !pick(options.showFirstLabel, 1)) || + (pos == max && !pick(options.showLastLabel, 0))), + width = categories && horiz && categories.length && + !labelOptions.step && !labelOptions.staggerLines && + !labelOptions.rotation && + plotWidth / categories.length || + !horiz && plotWidth / 2, + css, + label = this.label; + + + // get the string + str = labelFormatter.call({ + isFirst: pos == tickPositions[0], + isLast: pos == tickPositions[tickPositions.length - 1], + dateTimeLabelFormat: dateTimeLabelFormat, + value: (categories && categories[pos] ? categories[pos] : pos) + }); + + // prepare CSS + css = width && { width: (width - 2 * (labelOptions.padding || 10)) +PX }; + css = extend(css, labelOptions.style); + + // first call + if (label === UNDEFINED) { + this.label = + defined(str) && withLabel && labelOptions.enabled ? + renderer.text( + str, + 0, + 0 + ) + .attr({ + align: labelOptions.align, + rotation: labelOptions.rotation + }) + // without position absolute, IE export sometimes is wrong + .css(css) + .add(axisGroup): + null; + + // update + } else if (label) { + label.attr({ text: str }) + .css(css); + } + }, + /** + * Get the offset height or width of the label + */ + getLabelSize: function() { + var label = this.label; + return label ? + ((this.labelBBox = label.getBBox()))[horiz ? 'height' : 'width'] : + 0; + }, + /** + * Put everything in place + * + * @param index {Number} + * @param old {Boolean} Use old coordinates to prepare an animation into new position + */ + render: function(index, old) { + var tick = this, + major = !tick.minor, + label = tick.label, + pos = tick.pos, + labelOptions = options.labels, + gridLine = tick.gridLine, + gridLineWidth = major ? options.gridLineWidth : options.minorGridLineWidth, + gridLineColor = major ? options.gridLineColor : options.minorGridLineColor, + dashStyle = major ? + options.gridLineDashStyle : + options.minorGridLineDashStyle, + gridLinePath, + mark = tick.mark, + markPath, + tickLength = major ? options.tickLength : options.minorTickLength, + tickWidth = major ? options.tickWidth : (options.minorTickWidth || 0), + tickColor = major ? options.tickColor : options.minorTickColor, + tickPosition = major ? options.tickPosition : options.minorTickPosition, + step = labelOptions.step, + cHeight = old && oldChartHeight || chartHeight, + attribs, + x, + y; + + // get x and y position for ticks and labels + x = horiz ? + translate(pos + tickmarkOffset, null, null, old) + transB : + plotLeft + offset + (opposite ? (old && oldChartWidth || chartWidth) - marginRight - plotLeft : 0); + + y = horiz ? + cHeight - marginBottom + offset - (opposite ? plotHeight : 0) : + cHeight - translate(pos + tickmarkOffset, null, null, old) - transB; + + // create the grid line + if (gridLineWidth) { + gridLinePath = getPlotLinePath(pos + tickmarkOffset, gridLineWidth, old); + + if (gridLine === UNDEFINED) { + attribs = { + stroke: gridLineColor, + 'stroke-width': gridLineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + tick.gridLine = gridLine = + gridLineWidth ? + renderer.path(gridLinePath) + .attr(attribs).add(gridGroup) : + null; + } + if (gridLine && gridLinePath) { + gridLine.animate({ + d: gridLinePath + }); + } + } + + // create the tick mark + if (tickWidth) { + + // negate the length + if (tickPosition == 'inside') { + tickLength = -tickLength; + } + if (opposite) { + tickLength = -tickLength; + } + + markPath = renderer.crispLine([ + M, + x, + y, + L, + x + (horiz ? 0 : -tickLength), + y + (horiz ? tickLength : 0) + ], tickWidth); + + if (mark) { // updating + mark.animate({ + d: markPath + }); + } else { // first time + tick.mark = renderer.path( + markPath + ).attr({ + stroke: tickColor, + 'stroke-width': tickWidth + }).add(axisGroup); + } + } + + // the label is created on init - now move it into place + if (label) { + x = x + labelOptions.x - (tickmarkOffset && horiz ? + tickmarkOffset * transA * (reversed ? -1 : 1) : 0); + y = y + labelOptions.y - (tickmarkOffset && !horiz ? + tickmarkOffset * transA * (reversed ? 1 : -1) : 0); + + // vertically centered + if (!defined(labelOptions.y)) { + y += parseInt(label.styles.lineHeight) * 0.9 - label.getBBox().height / 2; + } + + + // correct for staggered labels + if (staggerLines) { + y += (index % staggerLines) * 16; + } + // apply step + if (step) { + // show those indices dividable by step + label[index % step ? 'hide' : 'show'](); + } + + label[tick.isNew ? 'attr' : 'animate']({ + x: x, + y: y + }); + + } + + tick.isNew = false; + }, + /** + * Destructor for the tick prototype + */ + destroy: function() { + var tick = this, + n; + for (n in tick) { + if (tick[n] && tick[n].destroy) { + tick[n].destroy(); + } + } + } + }; + + /** + * The object wrapper for plot lines and plot bands + * @param {Object} options + */ + function PlotLineOrBand(options) { + var plotLine = this; + if (options) { + plotLine.options = options; + plotLine.id = options.id; + } + + //plotLine.render() + return plotLine; + } + + PlotLineOrBand.prototype = { + + /** + * Render the plot line or plot band. If it is already existing, + * move it. + */ + render: function () { + var plotLine = this, + options = plotLine.options, + optionsLabel = options.label, + label = plotLine.label, + width = options.width, + to = options.to, + toPath, // bands only + from = options.from, + dashStyle = options.dashStyle, + svgElem = plotLine.svgElem, + path = [], + addEvent, + eventType, + xs, + ys, + x, + y, + color = options.color, + zIndex = options.zIndex, + events = options.events, + attribs; + + // plot line + if (width) { + path = getPlotLinePath(options.value, width); + attribs = { + stroke: color, + 'stroke-width': width + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + } + + // plot band + else if (defined(from) && defined(to)) { + // keep within plot area + from = mathMax(from, min); + to = mathMin(to, max); + + toPath = getPlotLinePath(to); + path = getPlotLinePath(from); + if (path && toPath) { + path.push( + toPath[4], + toPath[5], + toPath[1], + toPath[2] + ); + } else { // outside the axis area + path = null; + } + attribs = { + fill: color + }; + } else { + return; + } + // zIndex + if (defined(zIndex)) { + attribs.zIndex = zIndex; + } + + // common for lines and bands + if (svgElem) { + if (path) { + svgElem.animate({ + d: path + }, null, svgElem.onGetPath); + } else { + svgElem.hide(); + svgElem.onGetPath = function() { + svgElem.show(); + } + } + } else if (path && path.length) { + plotLine.svgElem = svgElem = renderer.path(path) + .attr(attribs).add(); + + // events + if (events) { + addEvent = function(eventType) { + svgElem.on(eventType, function(e) { + events[eventType].apply(plotLine, [e]); + }); + }; + for (eventType in events) { + addEvent(eventType); + } + } + } + + // the plot band/line label + if (optionsLabel && defined(optionsLabel.text) && path && path.length && plotWidth > 0 && plotHeight > 0) { + // apply defaults + optionsLabel = merge({ + align: horiz && toPath && 'center', + x: horiz ? !toPath && 4 : 10, + verticalAlign : !horiz && toPath && 'middle', + y: horiz ? toPath ? 16 : 10 : toPath ? 6 : -4, + rotation: horiz && !toPath && 90 + }, optionsLabel); + + // add the SVG element + if (!label) { + plotLine.label = label = renderer.text( + optionsLabel.text, + 0, + 0 + ) + .attr({ + align: optionsLabel.textAlign || optionsLabel.align, + rotation: optionsLabel.rotation, + zIndex: zIndex + }) + .css(optionsLabel.style) + .add(); + } + + // get the bounding box and align the label + xs = [path[1], path[4], path[6] || path[1]]; + ys = [path[2], path[5], path[7] || path[2]]; + x = mathMin.apply(math, xs); + y = mathMin.apply(math, ys); + + label.align(optionsLabel, false, { + x: x, + y: y, + width: mathMax.apply(math, xs) - x, + height: mathMax.apply(math, ys) - y + }); + label.show(); + + } else if (label) { // move out of sight + label.hide(); + } + + // chainable + return plotLine; + }, + + /** + * Remove the plot line or band + */ + destroy: function() { + var obj = this, + n; + + for (n in obj) { + if (obj[n] && obj[n].destroy) { + obj[n].destroy(); // destroy SVG wrappers + } + delete obj[n]; + } + // remove it from the lookup + erase(plotLinesAndBands, obj); + } + }; + + + /** + * Get the minimum and maximum for the series of each axis + */ + function getSeriesExtremes() { + var posStack = [], + negStack = [], + run; + + // reset dataMin and dataMax in case we're redrawing + dataMin = dataMax = null; + + // get an overview of what series are associated with this axis + associatedSeries = []; + + each(series, function(serie) { + run = false; + + + // match this axis against the series' given or implicated axis + each(['xAxis', 'yAxis'], function(strAxis) { + if ( + // the series is a cartesian type, and... + serie.isCartesian && + // we're in the right x or y dimension, and... + (strAxis == 'xAxis' && isXAxis || strAxis == 'yAxis' && !isXAxis) && ( + // the axis number is given in the options and matches this axis index, or + (serie.options[strAxis] == options.index) || + // the axis index is not given + (serie.options[strAxis] === UNDEFINED && options.index === 0) + ) + ) { + serie[strAxis] = axis; + associatedSeries.push(serie); + + // the series is visible, run the min/max detection + run = true; + } + }); + // ignore hidden series if opted + if (!serie.visible && optionsChart.ignoreHiddenSeries) { + run = false; + } + + if (run) { + + var stacking, + posPointStack, + negPointStack, + stackKey, + negKey; + + if (!isXAxis) { + stacking = serie.options.stacking; + usePercentage = stacking == 'percent'; + + // create a stack for this particular series type + if (stacking) { + stackKey = serie.type + pick(serie.options.stack, ''); + negKey = '-'+ stackKey; + serie.stackKey = stackKey; // used in translate + + posPointStack = posStack[stackKey] || []; // contains the total values for each x + posStack[stackKey] = posPointStack; + + negPointStack = negStack[negKey] || []; + negStack[negKey] = negPointStack; + } + if (usePercentage) { + dataMin = 0; + dataMax = 99; + } + } + if (serie.isCartesian) { // line, column etc. need axes, pie doesn't + each(serie.data, function(point, i) { + var pointX = point.x, + pointY = point.y, + isNegative = pointY < 0, + pointStack = isNegative ? negPointStack : posPointStack, + key = isNegative ? negKey : stackKey, + totalPos, + pointLow; + + // initial values + if (dataMin === null) { + + // start out with the first point + dataMin = dataMax = point[xOrY]; + } + + // x axis + if (isXAxis) { + if (pointX > dataMax) { + dataMax = pointX; + } else if (pointX < dataMin) { + dataMin = pointX; + } + } + + // y axis + else if (defined(pointY)) { + if (stacking) { + pointStack[pointX] = + defined(pointStack[pointX]) ? + pointStack[pointX] + pointY : pointY; + } + totalPos = pointStack ? pointStack[pointX] : pointY; + pointLow = pick(point.low, totalPos); + if (!usePercentage) { + if (totalPos > dataMax) { + dataMax = totalPos; + } else if (pointLow < dataMin) { + dataMin = pointLow; + } + } + if (stacking) { + // add the series + if (!stacks[key]) { + stacks[key] = {}; + } + stacks[key][pointX] = { + total: totalPos, + cum: totalPos + }; + } + } + }); + + + // For column, areas and bars, set the minimum automatically to zero + // and prevent that minPadding is added in setScale + if (/(area|column|bar)/.test(serie.type) && !isXAxis) { + if (dataMin >= 0) { + dataMin = 0; + ignoreMinPadding = true; + } else if (dataMax < 0) { + dataMax = 0; + ignoreMaxPadding = true; + } + } + } + } + }); + + } + + /** + * Translate from axis value to pixel position on the chart, or back + * + */ + translate = function(val, backwards, cvsCoord, old) { + var sign = 1, + cvsOffset = 0, + localA = old ? oldTransA : transA, + localMin = old ? oldMin : min, + returnValue; + + if (!localA) { + localA = transA; + } + + if (cvsCoord) { + sign *= -1; // canvas coordinates inverts the value + cvsOffset = axisLength; + } + if (reversed) { // reversed axis + sign *= -1; + cvsOffset -= sign * axisLength; + } + + if (backwards) { // reverse translation + if (reversed) { + val = axisLength - val; + } + returnValue = val / localA + localMin; // from chart pixel to value + + } else { // normal translation + returnValue = sign * (val - localMin) * localA + cvsOffset; // from value to chart pixel + } + + return returnValue; + }; + + /** + * Create the path for a plot line that goes from the given value on + * this axis, across the plot to the opposite side + * @param {Number} value + * @param {Number} lineWidth Used for calculation crisp line + * @param {Number] old Use old coordinates (for resizing and rescaling) + */ + getPlotLinePath = function(value, lineWidth, old) { + var x1, + y1, + x2, + y2, + translatedValue = translate(value, null, null, old), + cHeight = old && oldChartHeight || chartHeight, + cWidth = old && oldChartWidth || chartWidth, + skip; + + x1 = x2 = mathRound(translatedValue + transB); + y1 = y2 = mathRound(cHeight - translatedValue - transB); + + if (isNaN(translatedValue)) { // no min or max + skip = true; + + } else if (horiz) { + y1 = plotTop; + y2 = cHeight - marginBottom; + if (x1 < plotLeft || x1 > plotLeft + plotWidth) { + skip = true; + } + } else { + x1 = plotLeft; + x2 = cWidth - marginRight; + if (y1 < plotTop || y1 > plotTop + plotHeight) { + skip = true; + } + } + return skip ? + null : + renderer.crispLine([M, x1, y1, L, x2, y2], lineWidth || 0); + }; + + /** + * Take an interval and normalize it to multiples of 1, 2, 2.5 and 5 + * @param {Number} interval + */ + function normalizeTickInterval(interval, multiples) { + var normalized; + + // round to a tenfold of 1, 2, 2.5 or 5 + magnitude = multiples ? 1 : math.pow(10, mathFloor(math.log(interval) / math.LN10)); + normalized = interval / magnitude; + + // multiples for a linear scale + if (!multiples) { + multiples = [1, 2, 2.5, 5, 10]; + //multiples = [1, 2, 2.5, 4, 5, 7.5, 10]; + + // the allowDecimals option + if (options.allowDecimals === false) { + if (magnitude == 1) { + multiples = [1, 2, 5, 10]; + } else if (magnitude <= 0.1) { + multiples = [1 / magnitude]; + } + } + } + + // normalize the interval to the nearest multiple + for (var i = 0; i < multiples.length; i++) { + interval = multiples[i]; + if (normalized <= (multiples[i] + (multiples[i+1] || multiples[i])) / 2) { + break; + } + } + + // multiply back to the correct magnitude + interval *= magnitude; + + return interval; + } + + /** + * Set the tick positions to a time unit that makes sense, for example + * on the first of each month or on every Monday. + */ + function setDateTimeTickPositions() { + tickPositions = []; + var i, + useUTC = defaultOptions.global.useUTC, + oneSecond = 1000 / timeFactor, + oneMinute = 60000 / timeFactor, + oneHour = 3600000 / timeFactor, + oneDay = 24 * 3600000 / timeFactor, + oneWeek = 7 * 24 * 3600000 / timeFactor, + oneMonth = 30 * 24 * 3600000 / timeFactor, + oneYear = 31556952000 / timeFactor, + + units = [[ + 'second', // unit name + oneSecond, // fixed incremental unit + [1, 2, 5, 10, 15, 30] // allowed multiples + ], [ + 'minute', // unit name + oneMinute, // fixed incremental unit + [1, 2, 5, 10, 15, 30] // allowed multiples + ], [ + 'hour', // unit name + oneHour, // fixed incremental unit + [1, 2, 3, 4, 6, 8, 12] // allowed multiples + ], [ + 'day', // unit name + oneDay, // fixed incremental unit + [1, 2] // allowed multiples + ], [ + 'week', // unit name + oneWeek, // fixed incremental unit + [1, 2] // allowed multiples + ], [ + 'month', + oneMonth, + [1, 2, 3, 4, 6] + ], [ + 'year', + oneYear, + null + ]], + + unit = units[6], // default unit is years + interval = unit[1], + multiples = unit[2]; + + // loop through the units to find the one that best fits the tickInterval + for (i = 0; i < units.length; i++) { + unit = units[i]; + interval = unit[1]; + multiples = unit[2]; + + + if (units[i+1]) { + // lessThan is in the middle between the highest multiple and the next unit. + var lessThan = (interval * multiples[multiples.length - 1] + + units[i + 1][1]) / 2; + + // break and keep the current unit + if (tickInterval <= lessThan) { + break; + } + } + } + + // prevent 2.5 years intervals, though 25, 250 etc. are allowed + if (interval == oneYear && tickInterval < 5 * interval) { + multiples = [1, 2, 5]; + } + + // get the minimum value by flooring the date + var multitude = normalizeTickInterval(tickInterval / interval, multiples), + minYear, // used in months and years as a basis for Date.UTC() + minDate = new Date(min * timeFactor); + + minDate.setMilliseconds(0); + + if (interval >= oneSecond) { // second + minDate.setSeconds(interval >= oneMinute ? 0 : + multitude * mathFloor(minDate.getSeconds() / multitude)); + } + + if (interval >= oneMinute) { // minute + minDate[setMinutes](interval >= oneHour ? 0 : + multitude * mathFloor(minDate[getMinutes]() / multitude)); + } + + if (interval >= oneHour) { // hour + minDate[setHours](interval >= oneDay ? 0 : + multitude * mathFloor(minDate[getHours]() / multitude)); + } + + if (interval >= oneDay) { // day + minDate[setDate](interval >= oneMonth ? 1 : + multitude * mathFloor(minDate[getDate]() / multitude)); + } + + if (interval >= oneMonth) { // month + minDate[setMonth](interval >= oneYear ? 0 : + multitude * mathFloor(minDate[getMonth]() / multitude)); + minYear = minDate[getFullYear](); + } + + if (interval >= oneYear) { // year + minYear -= minYear % multitude; + minDate[setFullYear](minYear); + } + + // week is a special case that runs outside the hierarchy + if (interval == oneWeek) { + // get start of current week, independent of multitude + minDate[setDate](minDate[getDate]() - minDate[getDay]() + + options.startOfWeek); + } + + + // get tick positions + i = 1; // prevent crash just in case + minYear = minDate[getFullYear](); + var time = minDate.getTime() / timeFactor, + minMonth = minDate[getMonth](), + minDateDate = minDate[getDate](); + + // iterate and add tick positions at appropriate values + while (time < max && i < plotWidth) { + tickPositions.push(time); + + // if the interval is years, use Date.UTC to increase years + if (interval == oneYear) { + time = makeTime(minYear + i * multitude, 0) / timeFactor; + + // if the interval is months, use Date.UTC to increase months + } else if (interval == oneMonth) { + time = makeTime(minYear, minMonth + i * multitude) / timeFactor; + + // if we're using global time, the interval is not fixed as it jumps + // one hour at the DST crossover + } else if (!useUTC && (interval == oneDay || interval == oneWeek)) { + time = makeTime(minYear, minMonth, minDateDate + + i * multitude * (interval == oneDay ? 1 : 7)); + + // else, the interval is fixed and we use simple addition + } else { + time += interval * multitude; + } + + i++; + } + // push the last time + tickPositions.push(time); + + + // dynamic label formatter + dateTimeLabelFormat = options.dateTimeLabelFormats[unit[0]]; + } + + /** + * Fix JS round off float errors + * @param {Number} num + */ + function correctFloat(num) { + var invMag, ret = num; + if (defined(magnitude)) { + invMag = (magnitude < 1 ? mathRound(1 / magnitude) : 1) * 10; + ret = mathRound(num * invMag) / invMag; + } + return ret; + } + + /** + * Set the tick positions of a linear axis to round values like whole tens or every five. + */ + function setLinearTickPositions() { + + var i, + roundedMin = mathFloor(min / tickInterval) * tickInterval, + roundedMax = mathCeil(max / tickInterval) * tickInterval; + + tickPositions = []; + + // populate the intermediate values + i = correctFloat(roundedMin); + while (i <= roundedMax) { + tickPositions.push(i); + i = correctFloat(i + tickInterval); + } + + } + + /** + * Set the tick positions to round values and optionally extend the extremes + * to the nearest tick + */ + function setTickPositions(secondPass) { + var length, + catPad, + linkedParent, + linkedParentExtremes, + tickIntervalOption = options.tickInterval, + tickPixelIntervalOption = options.tickPixelInterval, + maxZoom = options.maxZoom || ( + isXAxis ? + mathMin(chart.smallestInterval * 5, dataMax - dataMin) : + null + ), + zoomOffset; + + + axisLength = horiz ? plotWidth : plotHeight; + + // linked axis gets the extremes from the parent axis + if (isLinked) { + linkedParent = chart[isXAxis ? 'xAxis' : 'yAxis'][options.linkedTo]; + linkedParentExtremes = linkedParent.getExtremes(); + min = pick(linkedParentExtremes.min, linkedParentExtremes.dataMin); + max = pick(linkedParentExtremes.max, linkedParentExtremes.dataMax); + } + + // initial min and max from the extreme data values + else { + min = pick(userSetMin, options.min, dataMin); + max = pick(userSetMax, options.max, dataMax); + } + + // maxZoom exceeded, just center the selection + if (max - min < maxZoom) { + zoomOffset = (maxZoom - max + min) / 2; + // if min and max options have been set, don't go beyond it + min = mathMax(min - zoomOffset, pick(options.min, min - zoomOffset), dataMin); + max = mathMin(min + maxZoom, pick(options.max, min + maxZoom), dataMax); + } + + // pad the values to get clear of the chart's edges + if (!categories && !usePercentage && !isLinked && defined(min) && defined(max)) { + length = (max - min) || 1; + if (!defined(options.min) && !defined(userSetMin) && minPadding && (dataMin < 0 || !ignoreMinPadding)) { + min -= length * minPadding; + } + if (!defined(options.max) && !defined(userSetMax) && maxPadding && (dataMax > 0 || !ignoreMaxPadding)) { + max += length * maxPadding; + } + } + + // get tickInterval + if (min == max) { + tickInterval = 1; + } else if (isLinked && !tickIntervalOption && + tickPixelIntervalOption == linkedParent.options.tickPixelInterval) { + tickInterval = linkedParent.tickInterval; + } else { + tickInterval = pick( + tickIntervalOption, + categories ? // for categoried axis, 1 is default, for linear axis use tickPix + 1 : + (max - min) * tickPixelIntervalOption / axisLength + ); + } + + if (!isDatetimeAxis && !defined(options.tickInterval)) { // linear + tickInterval = normalizeTickInterval(tickInterval); + } + axis.tickInterval = tickInterval; // record for linked axis + + // get minorTickInterval + minorTickInterval = options.minorTickInterval === 'auto' && tickInterval ? + tickInterval / 5 : options.minorTickInterval; + + // find the tick positions + if (isDatetimeAxis) { + setDateTimeTickPositions(); + } else { + setLinearTickPositions(); + } + + if (!isLinked) { + // pad categorised axis to nearest half unit + if (categories || (isXAxis && chart.hasColumn)) { + catPad = (categories ? 1 : tickInterval) * 0.5; + if (categories || !defined(pick(options.min, userSetMin))) { + min -= catPad; + } + if (categories || !defined(pick(options.max, userSetMax))) { + max += catPad; + } + } + + // reset min/max or remove extremes based on start/end on tick + var roundedMin = tickPositions[0], + roundedMax = tickPositions[tickPositions.length - 1]; + + if (options.startOnTick) { + min = roundedMin; + } else if (min > roundedMin) { + tickPositions.shift(); + } + + if (options.endOnTick) { + max = roundedMax; + } else if (max < roundedMax) { + tickPositions.pop(); + } + + // record the greatest number of ticks for multi axis + if (!maxTicks) { // first call, or maxTicks have been reset after a zoom operation + maxTicks = { + x: 0, + y: 0 + }; + } + + if (!isDatetimeAxis && tickPositions.length > maxTicks[xOrY]) { + maxTicks[xOrY] = tickPositions.length; + } + } + + + } + + /** + * When using multiple axes, adjust the number of ticks to match the highest + * number of ticks in that group + */ + function adjustTickAmount() { + + if (maxTicks && !isDatetimeAxis && !categories && !isLinked) { // only apply to linear scale + var oldTickAmount = tickAmount, + calculatedTickAmount = tickPositions.length; + + // set the axis-level tickAmount to use below + tickAmount = maxTicks[xOrY]; + + if (calculatedTickAmount < tickAmount) { + while (tickPositions.length < tickAmount) { + tickPositions.push( correctFloat( + tickPositions[tickPositions.length - 1] + tickInterval + )); + } + transA *= (calculatedTickAmount - 1) / (tickAmount - 1); + max = tickPositions[tickPositions.length - 1]; + + } + if (defined(oldTickAmount) && tickAmount != oldTickAmount) { + axis.isDirty = true; + } + } + + } + + /** + * Set the scale based on data min and max, user set min and max or options + * + */ + function setScale() { + var type, + i; + + oldMin = min; + oldMax = max; + + // get data extremes if needed + getSeriesExtremes(); + + // get fixed positions based on tickInterval + setTickPositions(); + + // the translation factor used in translate function + oldTransA = transA; + transA = axisLength / ((max - min) || 1); + + // reset stacks + if (!isXAxis) { + for (type in stacks) { + for (i in stacks[type]) { + stacks[type][i].cum = stacks[type][i].total; + } + } + } + + // mark as dirty if it is not already set to dirty and extremes have changed + if (!axis.isDirty) { + axis.isDirty = (min != oldMin || max != oldMax); + } + + } + + /** + * Set the extremes and optionally redraw + * @param {Number} newMin + * @param {Number} newMax + * @param {Boolean} redraw + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + function setExtremes(newMin, newMax, redraw, animation) { + + redraw = pick(redraw, true); // defaults to true + + fireEvent(axis, 'setExtremes', { // fire an event to enable syncing of multiple charts + min: newMin, + max: newMax + }, function() { // the default event handler + + userSetMin = newMin; + userSetMax = newMax; + + + // redraw + if (redraw) { + chart.redraw(animation); + } + }); + + } + + /** + * Get the actual axis extremes + */ + function getExtremes() { + return { + min: min, + max: max, + dataMin: dataMin, + dataMax: dataMax + }; + } + + /** + * Get the zero plane either based on zero or on the min or max value. + * Used in bar and area plots + */ + function getThreshold(threshold) { + if (min > threshold) { + threshold = min; + } else if (max < threshold) { + threshold = max; + } + + return translate(threshold, 0, 1); + } + + /** + * Add a plot band or plot line after render time + * + * @param options {Object} The plotBand or plotLine configuration object + */ + function addPlotBandOrLine(options) { + var obj = new PlotLineOrBand(options).render(); + plotLinesAndBands.push(obj); + return obj; + } + + /** + * Render the tick labels to a preliminary position to get their sizes + */ + function getOffset() { + + var hasData = associatedSeries.length && defined(min) && defined(max), + titleOffset = 0, + titleMargin = 0, + axisTitleOptions = options.title, + labelOptions = options.labels, + directionFactor = [-1, 1, 1, -1][side]; + + if (!axisGroup) { + axisGroup = renderer.g('axis') + .attr({ zIndex: 7 }) + .add(); + gridGroup = renderer.g('grid') + .attr({ zIndex: 1 }) + .add(); + } + + labelOffset = 0; // reset + + if (hasData || isLinked) { + each(tickPositions, function(pos) { + if (!ticks[pos]) { + ticks[pos] = new Tick(pos); + } else { + ticks[pos].addLabel(); // update labels depending on tick interval + } + + // left side must be align: right and right side must have align: left for labels + if (side === 0 || side == 2 || { 1: 'left', 3: 'right' }[side] == labelOptions.align) { + + // get the highest offset + labelOffset = mathMax( + ticks[pos].getLabelSize(), + labelOffset + ); + } + + }); + + if (staggerLines) { + labelOffset += (staggerLines - 1) * 16; + } + + } else { // doesn't have data + for (var n in ticks) { + ticks[n].destroy(); + delete ticks[n]; + } + } + + if (axisTitleOptions && axisTitleOptions.text) { + if (!axis.axisTitle) { + axis.axisTitle = renderer.text( + axisTitleOptions.text, + 0, + 0 + ) + .attr({ + zIndex: 7, + rotation: axisTitleOptions.rotation || 0, + align: + axisTitleOptions.textAlign || + { low: 'left', middle: 'center', high: 'right' }[axisTitleOptions.align] + }) + .css(axisTitleOptions.style) + .add(); + } + + titleOffset = axis.axisTitle.getBBox()[horiz ? 'height' : 'width']; + titleMargin = pick(axisTitleOptions.margin, horiz ? 5 : 10); + + } + + // handle automatic or user set offset + offset = directionFactor * (options.offset || axisOffset[side]); + + axisTitleMargin = + labelOffset + + (side != 2 && labelOffset && directionFactor * options.labels[horiz ? 'y' : 'x']) + + titleMargin; + + axisOffset[side] = mathMax( + axisOffset[side], + axisTitleMargin + titleOffset + directionFactor * offset + ); + + } + + /** + * Render the axis + */ + function render() { + var axisTitleOptions = options.title, + alternateGridColor = options.alternateGridColor, + lineWidth = options.lineWidth, + lineLeft, + lineTop, + linePath, + hasRendered = chart.hasRendered, + slideInTicks = hasRendered && defined(oldMin) && !isNaN(oldMin), + hasData = associatedSeries.length && defined(min) && defined(max); + + // update metrics + axisLength = horiz ? plotWidth : plotHeight; + transA = axisLength / ((max - min) || 1); + transB = horiz ? plotLeft : marginBottom; // translation addend + + // If the series has data draw the ticks. Else only the line and title + if (hasData || isLinked) { + + // minor ticks + if (minorTickInterval && !categories) { + var pos = min + (tickPositions[0] - min) % minorTickInterval; + for (pos; pos <= max; pos += minorTickInterval) { + if (!minorTicks[pos]) { + minorTicks[pos] = new Tick(pos, true); + } + + // render new ticks in old position + if (slideInTicks && minorTicks[pos].isNew) { + minorTicks[pos].render(null, true); + } + + + minorTicks[pos].isActive = true; + minorTicks[pos].render(); + } + } + + // major ticks + each(tickPositions, function(pos, i) { + // linked axes need an extra check to find out if + if (!isLinked || (pos >= min && pos <= max)) { + + // render new ticks in old position + if (slideInTicks && ticks[pos].isNew) { + ticks[pos].render(i, true); + } + + ticks[pos].isActive = true; + ticks[pos].render(i); + } + }); + + // alternate grid color + if (alternateGridColor) { + each(tickPositions, function(pos, i) { + if (i % 2 === 0 && pos < max) { + /*plotLinesAndBands.push(new PlotLineOrBand({ + from: pos, + to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max, + color: alternateGridColor + }));*/ + + if (!alternateBands[pos]) { + alternateBands[pos] = new PlotLineOrBand(); + } + alternateBands[pos].options = { + from: pos, + to: tickPositions[i + 1] !== UNDEFINED ? tickPositions[i + 1] : max, + color: alternateGridColor + }; + alternateBands[pos].render(); + alternateBands[pos].isActive = true; + } + }); + } + + // custom plot bands (behind grid lines) + /*if (!hasRendered) { // only first time + each(options.plotBands || [], function(plotBandOptions) { + plotLinesAndBands.push(new PlotLineOrBand( + extend({ zIndex: 1 }, plotBandOptions) + ).render()); + }); + }*/ + + + + + // custom plot lines and bands + if (!hasRendered) { // only first time + each((options.plotLines || []).concat(options.plotBands || []), function(plotLineOptions) { + plotLinesAndBands.push(new PlotLineOrBand(plotLineOptions).render()); + }); + } + + + + } // end if hasData + + // remove inactive ticks + each([ticks, minorTicks, alternateBands], function(coll) { + for (var pos in coll) { + if (!coll[pos].isActive) { + coll[pos].destroy(); + delete coll[pos]; + } else { + coll[pos].isActive = false; // reset + } + } + }); + + + + + // Static items. As the axis group is cleared on subsequent calls + // to render, these items are added outside the group. + // axis line + if (lineWidth) { + lineLeft = plotLeft + (opposite ? plotWidth : 0) + offset; + lineTop = chartHeight - marginBottom - (opposite ? plotHeight : 0) + offset; + + linePath = renderer.crispLine([ + M, + horiz ? + plotLeft: + lineLeft, + horiz ? + lineTop: + plotTop, + L, + horiz ? + chartWidth - marginRight : + lineLeft, + horiz ? + lineTop: + chartHeight - marginBottom + ], lineWidth); + if (!axisLine) { + axisLine = renderer.path(linePath) + .attr({ + stroke: options.lineColor, + 'stroke-width': lineWidth, + zIndex: 7 + }) + .add(); + } else { + axisLine.animate({ d: linePath }); + } + + } + + if (axis.axisTitle) { + // compute anchor points for each of the title align options + var margin = horiz ? plotLeft : plotTop, + fontSize = pInt(axisTitleOptions.style.fontSize || 12), + // the position in the length direction of the axis + alongAxis = { + low: margin + (horiz ? 0 : axisLength), + middle: margin + axisLength / 2, + high: margin + (horiz ? axisLength : 0) + }[axisTitleOptions.align], + + // the position in the perpendicular direction of the axis + offAxis = (horiz ? plotTop + plotHeight : plotLeft) + + (horiz ? 1 : -1) * // horizontal axis reverses the margin + (opposite ? -1 : 1) * // so does opposite axes + axisTitleMargin + + //(isIE ? fontSize / 3 : 0)+ // preliminary fix for vml's centerline + (side == 2 ? fontSize : 0); + + axis.axisTitle[hasRendered ? 'animate' : 'attr']({ + x: horiz ? + alongAxis: + offAxis + (opposite ? plotWidth : 0) + offset + + (axisTitleOptions.x || 0), // x + y: horiz ? + offAxis - (opposite ? plotHeight : 0) + offset: + alongAxis + (axisTitleOptions.y || 0) // y + }); + + } + + axis.isDirty = false; + } + + /** + * Remove a plot band or plot line from the chart by id + * @param {Object} id + */ + function removePlotBandOrLine(id) { + var i = plotLinesAndBands.length; + while (i--) { + if (plotLinesAndBands[i].id == id) { + plotLinesAndBands[i].destroy(); + } + } + } + + /** + * Redraw the axis to reflect changes in the data or axis extremes + */ + function redraw() { + + // hide tooltip and hover states + if (tracker.resetTracker) { + tracker.resetTracker(); + } + + // render the axis + render(); + + // move plot lines and bands + each(plotLinesAndBands, function(plotLine) { + plotLine.render(); + }); + + // mark associated series as dirty and ready for redraw + each(associatedSeries, function(series) { + series.isDirty = true; + }); + + } + + /** + * Set new axis categories and optionally redraw + * @param {Array} newCategories + * @param {Boolean} doRedraw + */ + function setCategories(newCategories, doRedraw) { + // set the categories + axis.categories = categories = newCategories; + + // force reindexing tooltips + each(associatedSeries, function(series) { + series.translate(); + series.setTooltipPoints(true); + }); + + + // optionally redraw + axis.isDirty = true; + + if (pick(doRedraw, true)) { + chart.redraw(); + } + } + + + + // Run Axis + + // inverted charts have reversed xAxes as default + if (inverted && isXAxis && reversed === UNDEFINED) { + reversed = true; + } + + + // expose some variables + extend(axis, { + addPlotBand: addPlotBandOrLine, + addPlotLine: addPlotBandOrLine, + adjustTickAmount: adjustTickAmount, + categories: categories, + getExtremes: getExtremes, + getPlotLinePath: getPlotLinePath, + getThreshold: getThreshold, + isXAxis: isXAxis, + options: options, + plotLinesAndBands: plotLinesAndBands, + getOffset: getOffset, + render: render, + setCategories: setCategories, + setExtremes: setExtremes, + setScale: setScale, + setTickPositions: setTickPositions, + translate: translate, + redraw: redraw, + removePlotBand: removePlotBandOrLine, + removePlotLine: removePlotBandOrLine, + reversed: reversed, + stacks: stacks + }); + + // register event listeners + for (eventType in events) { + addEvent(axis, eventType, events[eventType]); + } + + // set min and max + setScale(); + + } // end Axis + + + /** + * The toolbar object + * + * @param {Object} chart + */ + function Toolbar(chart) { + var buttons = {}; + + function add(id, text, title, fn) { + if (!buttons[id]) { + var button = renderer.text( + text, + 0, + 0 + ) + .css(options.toolbar.itemStyle) + .align({ + align: 'right', + x: - marginRight - 20, + y: plotTop + 30 + }) + .on('click', fn) + /*.on('touchstart', function(e) { + e.stopPropagation(); // don't fire the container event + fn(); + })*/ + .attr({ + align: 'right', + zIndex: 20 + }) + .add(); + buttons[id] = button; + } + } + function remove(id) { + discardElement(buttons[id].element); + buttons[id] = null; + } + + // public + return { + add: add, + remove: remove + }; + } + + /** + * The tooltip object + * @param {Object} options Tooltip options + */ + function Tooltip (options) { + var currentSeries, + borderWidth = options.borderWidth, + crosshairsOptions = options.crosshairs, + crosshairs = [], + style = options.style, + shared = options.shared, + padding = pInt(style.padding), + boxOffLeft = borderWidth + padding, // off left/top position as IE can't + //properly handle negative positioned shapes + tooltipIsHidden = true, + boxWidth, + boxHeight, + currentX = 0, + currentY = 0; + + // remove padding CSS and apply padding on box instead + style.padding = 0; + + // create the elements + var group = renderer.g('tooltip') + .attr({ zIndex: 8 }) + .add(), + + box = renderer.rect(boxOffLeft, boxOffLeft, 0, 0, options.borderRadius, borderWidth) + .attr({ + fill: options.backgroundColor, + 'stroke-width': borderWidth + }) + .add(group) + .shadow(options.shadow), + label = renderer.text('', padding + boxOffLeft, pInt(style.fontSize) + padding + boxOffLeft) + .attr({ zIndex: 1 }) + .css(style) + .add(group); + + group.hide(); + + /** + * In case no user defined formatter is given, this will be used + */ + function defaultFormatter() { + var pThis = this, + items = pThis.points || splat(pThis), + xAxis = items[0].series.xAxis, + x = pThis.x, + isDateTime = xAxis && xAxis.options.type == 'datetime', + useHeader = isString(x) || isDateTime, + series, + s; + + // build the header + s = useHeader ? + ['<span style="font-size: 10px">', + (isDateTime ? dateFormat('%A, %b %e, %Y', x) : x), + '</span><br/>'] : []; + + // build the values + each(items, function(item) { + s.push(item.point.tooltipFormatter(useHeader)); + }); + return s.join(''); + } + + /** + * Provide a soft movement for the tooltip + * + * @param {Number} finalX + * @param {Number} finalY + */ + function move(finalX, finalY) { + + currentX = tooltipIsHidden ? finalX : (2 * currentX + finalX) / 3; + currentY = tooltipIsHidden ? finalY : (currentY + finalY) / 2; + + group.translate(currentX, currentY); + + + // run on next tick of the mouse tracker + if (mathAbs(finalX - currentX) > 1 || mathAbs(finalY - currentY) > 1) { + tooltipTick = function() { + move(finalX, finalY); + }; + } else { + tooltipTick = null; + } + } + + /** + * Hide the tooltip + */ + function hide() { + if (!tooltipIsHidden) { + var hoverPoints = chart.hoverPoints; + + group.hide(); + + each(crosshairs, function(crosshair) { + if (crosshair) { + crosshair.hide(); + } + }); + + // hide previous hoverPoints and set new + if (hoverPoints) { + each (hoverPoints, function(point) { + point.setState(); + }); + } + chart.hoverPoints = null; + + + tooltipIsHidden = true; + } + + } + + /** + * Refresh the tooltip's text and position. + * @param {Object} point + * + */ + function refresh(point) { + var x, + y, + boxX, + boxY, + show, + bBox, + plotX, + plotY = 0, + textConfig = {}, + text, + pointConfig = [], + tooltipPos = point.tooltipPos, + formatter = options.formatter || defaultFormatter, + hoverPoints = chart.hoverPoints, + getConfig = function(point) { + return { + series: point.series, + point: point, + x: point.category, + y: point.y, + percentage: point.percentage, + total: point.total || point.stackTotal + }; + }; + + // shared tooltip, array is sent over + if (shared) { + + // hide previous hoverPoints and set new + if (hoverPoints) { + each (hoverPoints, function(point) { + point.setState(); + }); + } + chart.hoverPoints = point; + + each(point, function(item, i) { + /*var series = item.series, + hoverPoint = series.hoverPoint; + if (hoverPoint) { + hoverPoint.setState(); + } + series.hoverPoint = item;*/ + item.setState(HOVER_STATE); + plotY += item.plotY; // for average + + pointConfig.push(getConfig(item)); + }); + + plotX = point[0].plotX; + plotY = mathRound(plotY) / point.length; // mathRound because Opera 10 has problems here + + textConfig = { + x: point[0].category + }; + textConfig.points = pointConfig; + point = point[0]; + + // single point tooltip + } else { + textConfig = getConfig(point); + } + text = formatter.call(textConfig); + + // register the current series + currentSeries = point.series; + + // get the reference point coordinates (pie charts use tooltipPos) + plotX = shared ? plotX : point.plotX; + plotY = shared ? plotY : point.plotY; + x = mathRound(tooltipPos ? tooltipPos[0] : (inverted ? plotWidth - plotY : plotX)); + y = mathRound(tooltipPos ? tooltipPos[1] : (inverted ? plotHeight - plotX : plotY)); + + + // hide tooltip if the point falls outside the plot + show = shared || !point.series.isCartesian || isInsidePlot(x, y); + + // update the inner HTML + if (text === false || !show) { + hide(); + } else { + + // show it + if (tooltipIsHidden) { + group.show(); + tooltipIsHidden = false; + } + + // update text + label.attr({ + text: text + }); + + // get the bounding box + bBox = label.getBBox(); + boxWidth = bBox.width + 2 * padding; + boxHeight = bBox.height + 2 * padding; + + // set the size of the box + box.attr({ + width: boxWidth, + height: boxHeight, + stroke: options.borderColor || point.color || currentSeries.color || '#606060' + }); + + // keep the box within the chart area + boxX = x - boxWidth + plotLeft - 25; + boxY = y - boxHeight + plotTop + 10; + + // it is too far to the left, adjust it + if (boxX < 7) { + boxX = 7; + boxY -= 30; + } + + + if (boxY < 5) { + boxY = 5; // above + } else if (boxY + boxHeight > chartHeight) { + boxY = chartHeight - boxHeight - 5; // below + } + + // do the move + move(mathRound(boxX - boxOffLeft), mathRound(boxY - boxOffLeft)); + + + } + + + // crosshairs + if (crosshairsOptions) { + crosshairsOptions = splat(crosshairsOptions); // [x, y] + + var path, + i = crosshairsOptions.length, + attribs, + axis; + + while (i--) { + if (crosshairsOptions[i] && (axis = point.series[i ? 'yAxis' : 'xAxis'])) { + path = axis + .getPlotLinePath(point[i ? 'y' : 'x'], 1); + if (crosshairs[i]) { + crosshairs[i].attr({ d: path, visibility: VISIBLE }); + + } else { + attribs = { + 'stroke-width': crosshairsOptions[i].width || 1, + stroke: crosshairsOptions[i].color || '#C0C0C0', + zIndex: 2 + }; + if (crosshairsOptions[i].dashStyle) { + attribs.dashstyle = crosshairsOptions[i].dashStyle; + } + crosshairs[i] = renderer.path(path) + .attr(attribs) + .add(); + } + } + } + } + } + + + + // public members + return { + shared: shared, + refresh: refresh, + hide: hide + }; + } + + /** + * The mouse tracker object + * @param {Object} chart + * @param {Object} options + */ + function MouseTracker (chart, options) { + + + var mouseDownX, + mouseDownY, + hasDragged, + selectionMarker, + zoomType = optionsChart.zoomType, + zoomX = /x/.test(zoomType), + zoomY = /y/.test(zoomType), + zoomHor = zoomX && !inverted || zoomY && inverted, + zoomVert = zoomY && !inverted || zoomX && inverted; + + /** + * Add crossbrowser support for chartX and chartY + * @param {Object} e The event object in standard browsers + */ + function normalizeMouseEvent(e) { + var ePos; + + // common IE normalizing + e = e || win.event; + if (!e.target) { + e.target = e.srcElement; + } + + // iOS + ePos = e.touches ? e.touches.item(0) : e; + + // in certain cases, get mouse position + if (e.type != 'mousemove' || win.opera) { // only Opera needs position on mouse move, see below + chartPosition = getPosition(container); + } + + // chartX and chartY + if (isIE) { // IE including IE9 that has chartX but in a different meaning + e.chartX = e.x; + e.chartY = e.y; + } else { + if (ePos.layerX === UNDEFINED) { // Opera and iOS + e.chartX = ePos.pageX - chartPosition.left; + e.chartY = ePos.pageY - chartPosition.top; + } else { + e.chartX = e.layerX; + e.chartY = e.layerY; + } + } + + return e; + } + + /** + * Get the click position in terms of axis values. + * + * @param {Object} e A mouse event + */ + function getMouseCoordinates(e) { + var coordinates = { + xAxis: [], + yAxis: [] + }; + each(axes, function(axis, i) { + var translate = axis.translate, + isXAxis = axis.isXAxis, + isHorizontal = inverted ? !isXAxis : isXAxis; + + coordinates[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + value: translate( + isHorizontal ? + e.chartX - plotLeft : + plotHeight - e.chartY + plotTop, + true + ) + }); + }); + return coordinates; + } + + /** + * With line type charts with a single tracker, get the point closest to the mouse + */ + function onmousemove (e) { + var point, + points, + hoverPoint = chart.hoverPoint, + hoverSeries = chart.hoverSeries, + i, + j, + distance = chartWidth, + index = inverted ? e.chartY : e.chartX - plotLeft; // wtf? + + // shared tooltip + if (tooltip && options.shared) { + points = []; + + // loop over all series and find the ones with points closest to the mouse + i = series.length; + for (j = 0; j < i; j++) { + if (series[j].visible && series[j].tooltipPoints.length) { + point = series[j].tooltipPoints[index]; + point._dist = mathAbs(index - point.plotX); + distance = mathMin(distance, point._dist); + points.push(point); + } + } + // remove furthest points + i = points.length; + while (i--) { + if (points[i]._dist > distance) { + points.splice(i, 1); + } + } + // refresh the tooltip if necessary + if (points.length && (points[0].plotX != hoverX)) { + tooltip.refresh(points); + hoverX = points[0].plotX; + } + } + + // separate tooltip and general mouse events + if (hoverSeries && hoverSeries.tracker) { // only use for line-type series with common tracker + + // get the point + point = hoverSeries.tooltipPoints[index]; + + // a new point is hovered, refresh the tooltip + if (point && point != hoverPoint) { + + // trigger the events + point.onMouseOver(); + + } + } + } + + + + /** + * Reset the tracking by hiding the tooltip, the hover series state and the hover point + */ + function resetTracker() { + var hoverSeries = chart.hoverSeries, + hoverPoint = chart.hoverPoint; + + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + if (hoverSeries) { + hoverSeries.onMouseOut(); + } + + if (tooltip) { + tooltip.hide(); + } + + hoverX = null; + } + + /** + * Mouse up or outside the plot area + */ + function drop() { + if (selectionMarker) { + var selectionData = { + xAxis: [], + yAxis: [] + }, + selectionBox = selectionMarker.getBBox(), + selectionLeft = selectionBox.x - plotLeft, + selectionTop = selectionBox.y - plotTop; + + + // a selection has been made + if (hasDragged) { + + // record each axis' min and max + each(axes, function(axis, i) { + var translate = axis.translate, + isXAxis = axis.isXAxis, + isHorizontal = inverted ? !isXAxis : isXAxis, + selectionMin = translate( + isHorizontal ? + selectionLeft : + plotHeight - selectionTop - selectionBox.height, + true + ), + selectionMax = translate( + isHorizontal ? + selectionLeft + selectionBox.width : + plotHeight - selectionTop, + true + ); + + selectionData[isXAxis ? 'xAxis' : 'yAxis'].push({ + axis: axis, + min: mathMin(selectionMin, selectionMax), // for reversed axes + max: mathMax(selectionMin, selectionMax) + }); + + }); + fireEvent(chart, 'selection', selectionData, zoom); + + } + selectionMarker = selectionMarker.destroy(); + } + + chart.mouseIsDown = mouseIsDown = hasDragged = false; + removeEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); + + } + + /** + * Set the JS events on the container element + */ + function setDOMEvents () { + var lastWasOutsidePlot = true; + + /* + * Record the starting position of a dragoperation + */ + container.onmousedown = function(e) { + e = normalizeMouseEvent(e); + + // record the start position + //e.preventDefault && e.preventDefault(); + + chart.mouseIsDown = mouseIsDown = true; + mouseDownX = e.chartX; + mouseDownY = e.chartY; + + addEvent(doc, hasTouch ? 'touchend' : 'mouseup', drop); + }; + + // The mousemove, touchmove and touchstart event handler + var mouseMove = function(e) { + + // let the system handle multitouch operations like two finger scroll + // and pinching + if (e && e.touches && e.touches.length > 1) { + return; + } + + // normalize + e = normalizeMouseEvent(e); + if (!hasTouch) { // not for touch devices + e.returnValue = false; + } + + var chartX = e.chartX, + chartY = e.chartY, + isOutsidePlot = !isInsidePlot(chartX - plotLeft, chartY - plotTop); + + // on touch devices, only trigger click if a handler is defined + if (hasTouch && e.type == 'touchstart') { + if (attr(e.target, 'isTracker')) { + if (!chart.runTrackerClick) { + e.preventDefault(); + } + } else if (!runChartClick && !isOutsidePlot) { + e.preventDefault(); + } + } + + // cancel on mouse outside + if (isOutsidePlot) { + + if (!lastWasOutsidePlot) { + // reset the tracker + resetTracker(); + } + + // drop the selection if any and reset mouseIsDown and hasDragged + //drop(); + if (chartX < plotLeft) { + chartX = plotLeft; + } else if (chartX > plotLeft + plotWidth) { + chartX = plotLeft + plotWidth; + } + + if (chartY < plotTop) { + chartY = plotTop; + } else if (chartY > plotTop + plotHeight) { + chartY = plotTop + plotHeight; + } + + } + + if (mouseIsDown && e.type != 'touchstart') { // make selection + + // determine if the mouse has moved more than 10px + if ((hasDragged = Math.sqrt( + Math.pow(mouseDownX - chartX, 2) + + Math.pow(mouseDownY - chartY, 2) + ) > 10)) { + + // make a selection + if (hasCartesianSeries && (zoomX || zoomY) && + isInsidePlot(mouseDownX - plotLeft, mouseDownY - plotTop)) { + if (!selectionMarker) { + selectionMarker = renderer.rect( + plotLeft, + plotTop, + zoomHor ? 1 : plotWidth, + zoomVert ? 1 : plotHeight, + 0 + ) + .attr({ + fill: 'rgba(69,114,167,0.25)', + zIndex: 7 + }) + .add(); + } + } + + // adjust the width of the selection marker + if (selectionMarker && zoomHor) { + var xSize = chartX - mouseDownX; + selectionMarker.attr({ + width: mathAbs(xSize), + x: (xSize > 0 ? 0 : xSize) + mouseDownX + }); + } + // adjust the height of the selection marker + if (selectionMarker && zoomVert) { + var ySize = chartY - mouseDownY; + selectionMarker.attr({ + height: mathAbs(ySize), + y: (ySize > 0 ? 0 : ySize) + mouseDownY + }); + } + } + + } else if (!isOutsidePlot) { + // show the tooltip + onmousemove(e); + } + + lastWasOutsidePlot = isOutsidePlot; + + // when outside plot, allow touch-drag by returning true + return isOutsidePlot || !hasCartesianSeries; + }; + + /* + * When the mouse enters the container, run mouseMove + */ + container.onmousemove = mouseMove; + + /* + * When the mouse leaves the container, hide the tracking (tooltip). + */ + addEvent(container, 'mouseleave', resetTracker); + + + container.ontouchstart = function(e) { + // For touch devices, use touchmove to zoom + if (zoomX || zoomY) { + container.onmousedown(e); + } + // Show tooltip and prevent the lower mouse pseudo event + mouseMove(e); + }; + + /* + * Allow dragging the finger over the chart to read the values on touch + * devices + */ + container.ontouchmove = mouseMove; + + /* + * Allow dragging the finger over the chart to read the values on touch + * devices + */ + container.ontouchend = function() { + if (hasDragged) { + resetTracker(); + } + }; + + + // MooTools 1.2.3 doesn't fire this in IE when using addEvent + container.onclick = function(e) { + var hoverPoint = chart.hoverPoint; + e = normalizeMouseEvent(e); + + e.cancelBubble = true; // IE specific + + + if (!hasDragged) { + if (hoverPoint && attr(e.target, 'isTracker')) { + var plotX = hoverPoint.plotX, + plotY = hoverPoint.plotY; + + // add page position info + extend(hoverPoint, { + pageX: chartPosition.left + plotLeft + + (inverted ? plotWidth - plotY : plotX), + pageY: chartPosition.top + plotTop + + (inverted ? plotHeight - plotX : plotY) + }); + + // the series click event + fireEvent(hoverPoint.series, 'click', extend(e, { + point: hoverPoint + })); + + // the point click event + hoverPoint.firePointEvent('click', e); + + } else { + extend(e, getMouseCoordinates(e)); + + // fire a click event in the chart + if (isInsidePlot(e.chartX - plotLeft, e.chartY - plotTop)) { + fireEvent(chart, 'click', e); + } + } + + + } + // reset mouseIsDown and hasDragged + hasDragged = false; + }; + + } + + /** + * Create the image map that listens for mouseovers + */ + placeTrackerGroup = function() { + + // first create - plot positions is not final at this stage + if (!trackerGroup) { + chart.trackerGroup = trackerGroup = renderer.g('tracker') + .attr({ zIndex: 9 }) + .add(); + + // then position - this happens on load and after resizing and changing + // axis or box positions + } else { + trackerGroup.translate(plotLeft, plotTop); + if (inverted) { + trackerGroup.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }).invert(); + } + } + }; + + + // Run MouseTracker + placeTrackerGroup(); + if (options.enabled) { + chart.tooltip = tooltip = Tooltip(options); + } + + setDOMEvents(); + + // set the fixed interval ticking for the smooth tooltip + tooltipInterval = setInterval(function() { + if (tooltipTick) { + tooltipTick(); + } + }, 32); + + // expose properties + extend(this, { + zoomX: zoomX, + zoomY: zoomY, + resetTracker: resetTracker + }); + } + + + + /** + * The overview of the chart's series + * @param {Object} chart + */ + var Legend = function(chart) { + + var options = chart.options.legend; + + if (!options.enabled) { + return; + } + + var horizontal = options.layout == 'horizontal', + symbolWidth = options.symbolWidth, + symbolPadding = options.symbolPadding, + allItems, + style = options.style, + itemStyle = options.itemStyle, + itemHoverStyle = options.itemHoverStyle, + itemHiddenStyle = options.itemHiddenStyle, + padding = pInt(style.padding), + rightPadding = 20, + //lineHeight = options.lineHeight || 16, + y = 18, + initialItemX = 4 + padding + symbolWidth + symbolPadding, + itemX, + itemY, + lastItemY, + itemHeight = 0, + box, + legendBorderWidth = options.borderWidth, + legendBackgroundColor = options.backgroundColor, + legendGroup, + offsetWidth, + widthOption = options.width, + series = chart.series, + reversedLegend = options.reversed; + + + + /** + * Set the colors for the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + function colorizeItem(item, visible) { + var legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + hiddenColor = itemHiddenStyle.color, + textColor = visible ? options.itemStyle.color : hiddenColor, + symbolColor = visible ? item.color : hiddenColor; + if (legendItem) { + legendItem.css({ fill: textColor }); + } + if (legendLine) { + legendLine.attr({ stroke: symbolColor }); + } + if (legendSymbol) { + legendSymbol.attr({ + stroke: symbolColor, + fill: symbolColor + }); + } + } + + /** + * Position the legend item + * @param {Object} item A Series or Point instance + * @param {Object} visible Dimmed or colored + */ + function positionItem(item, itemX, itemY) { + var legendItem = item.legendItem, + legendLine = item.legendLine, + legendSymbol = item.legendSymbol, + checkbox = item.checkbox; + if (legendItem) { + legendItem.attr({ + x: itemX, + y: itemY + }); + } + if (legendLine) { + legendLine.translate(itemX, itemY - 4); + } + if (legendSymbol) { + legendSymbol.attr({ + x: itemX + legendSymbol.xOff, + y: itemY + legendSymbol.yOff + }); + } + if (checkbox) { + checkbox.x = itemX; + checkbox.y = itemY; + } + } + + /** + * Destroy a single legend item + * @param {Object} item The series or point + */ + function destroyItem(item) { + var checkbox = item.checkbox; + + // pull out from the array + //erase(allItems, item); + + // destroy SVG elements + each(['legendItem', 'legendLine', 'legendSymbol'], function(key) { + if (item[key]) { + item[key].destroy(); + } + }); + + if (checkbox) { + discardElement(item.checkbox); + } + + + } + + + /** + * Position the checkboxes after the width is determined + */ + function positionCheckboxes() { + each(allItems, function(item) { + var checkbox = item.checkbox; + if (checkbox) { + css(checkbox, { + left: (legendGroup.attr('translateX') + item.legendItemWidth + checkbox.x - 40) +PX, + top: (legendGroup.attr('translateY') + checkbox.y - 11) + PX + }); + } + }); + } + + /** + * Render a single specific legend item + * @param {Object} item A series or point + */ + function renderItem(item) { + var bBox, + itemWidth, + legendSymbol, + symbolX, + symbolY, + attribs, + simpleSymbol, + li = item.legendItem, + series = item.series || item, + i = allItems.length; + + + if (!li) { // generate it once, later move it + + // let these series types use a simple symbol + simpleSymbol = /^(bar|pie|area|column)$/.test(series.type); + + // generate the list item text + item.legendItem = li = renderer.text( + options.labelFormatter.call(item), + 0, + 0 + ) + .css(item.visible ? itemStyle : itemHiddenStyle) + .on('mouseover', function() { + item.setState(HOVER_STATE); + li.css(itemHoverStyle); + }) + .on('mouseout', function() { + li.css(item.visible ? itemStyle : itemHiddenStyle); + item.setState(); + }) + .on('click', function(event) { + var strLegendItemClick = 'legendItemClick', + fnLegendItemClick = function() { + item.setVisible(); + }; + + // click the name or symbol + if (item.firePointEvent) { // point + item.firePointEvent(strLegendItemClick, null, fnLegendItemClick); + } else { + fireEvent(item, strLegendItemClick, null, fnLegendItemClick); + } + }) + .attr({ zIndex: 2 }) + .add(legendGroup); + + // draw the line + if (!simpleSymbol && item.options && item.options.lineWidth) { + var itemOptions = item.options; + attribs = { + 'stroke-width': itemOptions.lineWidth, + zIndex: 2 + }; + if (itemOptions.dashStyle) { + attribs.dashstyle = itemOptions.dashStyle; + } + item.legendLine = renderer.path([ + M, + -symbolWidth - symbolPadding, + 0, + L, + -symbolPadding, + 0 + ]) + .attr(attribs) + .add(legendGroup); + } + + // draw a simple symbol + if (simpleSymbol) { // bar|pie|area|column + //legendLayer.drawRect( + legendSymbol = renderer.rect( + (symbolX = -symbolWidth - symbolPadding), + (symbolY = -11), + symbolWidth, + 12, + 2 + ).attr({ + 'stroke-width': 0, + zIndex: 3 + }).add(legendGroup); + } + + // draw the marker + else if (item.options && item.options.marker && item.options.marker.enabled) { + legendSymbol = renderer.symbol( + item.symbol, + (symbolX = -symbolWidth / 2 - symbolPadding), + (symbolY = -4), + item.options.marker.radius + ) + .attr(item.pointAttr[NORMAL_STATE]) + .attr({ zIndex: 3 }) + .add(legendGroup); + + + } + if (legendSymbol) { + legendSymbol.xOff = symbolX; + legendSymbol.yOff = symbolY; + } + + item.legendSymbol = legendSymbol; + + // colorize the items + colorizeItem(item, item.visible); + + + // add the HTML checkbox on top + if (item.options && item.options.showCheckbox) { + item.checkbox = createElement('input', { + type: 'checkbox', + checked: item.selected, + defaultChecked: item.selected // required by IE7 + }, options.itemCheckboxStyle, container); + + addEvent(item.checkbox, 'click', function(event) { + var target = event.target; + fireEvent(item, 'checkboxClick', { + checked: target.checked + }, + function() { + item.select(); + } + ); + }); + } + } + + + // calculate the positions for the next line + bBox = li.getBBox(); + + itemWidth = item.legendItemWidth = + options.itemWidth || symbolWidth + symbolPadding + bBox.width + rightPadding; + itemHeight = bBox.height; + + // if the item exceeds the width, start a new line + if (horizontal && itemX - initialItemX + itemWidth > + (widthOption || (chartWidth - 2 * padding - initialItemX))) { + itemX = initialItemX; + itemY += itemHeight; + } + lastItemY = itemY; + + // position the newly generated or reordered items + positionItem(item, itemX, itemY); + + // advance + if (horizontal) { + itemX += itemWidth; + } else { + itemY += itemHeight; + } + + // the width of the widest item + offsetWidth = widthOption || mathMax( + horizontal ? itemX - initialItemX : itemWidth, + offsetWidth + ); + + + + // add it all to an array to use below + allItems.push(item); + } + + /** + * Render the legend. This method can be called both before and after + * chart.render. If called after, it will only rearrange items instead + * of creating new ones. + */ + function renderLegend() { + itemX = initialItemX; + itemY = y; + offsetWidth = 0; + lastItemY = 0; + + allItems = []; + + if (!legendGroup) { + legendGroup = renderer.g('legend') + .attr({ zIndex: 7 }) + .add(); + } + + + // add HTML for each series + if (reversedLegend) { + series.reverse(); + } + each(series, function(serie) { + if (!serie.options.showInLegend) { + return; + } + + // use points or series for the legend item depending on legendType + var items = (serie.options.legendType == 'point') ? + serie.data : [serie]; + + // render all items + each(items, renderItem); + }); + if (reversedLegend) { // restore + series.reverse(); + } + + + + // Draw the border + legendWidth = widthOption || offsetWidth; + legendHeight = lastItemY - y + itemHeight; + + if (legendBorderWidth || legendBackgroundColor) { + legendWidth += 2 * padding; + legendHeight += 2 * padding; + + if (!box) { + box = renderer.rect( + 0, + 0, + legendWidth, + legendHeight, + options.borderRadius, + legendBorderWidth || 0 + ).attr({ + stroke: options.borderColor, + 'stroke-width': legendBorderWidth || 0, + fill: legendBackgroundColor || NONE + }) + .add(legendGroup) + .shadow(options.shadow); + + } else if (legendWidth > 0 && legendHeight > 0) { + box.animate( + box.crisp(null, null, null, legendWidth, legendHeight) + ); + } + + // hide the border if no items + box[allItems.length ? 'show' : 'hide'](); + } + + // 1.x compatibility: positioning based on style + var props = ['left', 'right', 'top', 'bottom'], + prop, + i = 4; + while(i--) { + prop = props[i]; + if (style[prop] && style[prop] != 'auto') { + options[i < 2 ? 'align' : 'verticalAlign'] = prop; + options[i < 2 ? 'x' : 'y'] = pInt(style[prop]) * (i % 2 ? -1 : 1); + } + } + + legendGroup.align(extend(options, { + width: legendWidth, + height: legendHeight + }), true, spacingBox); + + if (!isResizing) { + positionCheckboxes(); + } + } + + + // run legend + renderLegend(); + + // move checkboxes + addEvent(chart, 'endResize', positionCheckboxes); + + // expose + return { + colorizeItem: colorizeItem, + destroyItem: destroyItem, + renderLegend: renderLegend + }; + }; + + + + + + + /** + * Initialize an individual series, called internally before render time + */ + function initSeries(options) { + var type = options.type || optionsChart.type || optionsChart.defaultSeriesType, + typeClass = seriesTypes[type], + serie, + hasRendered = chart.hasRendered; + + // an inverted chart can't take a column series and vice versa + if (hasRendered) { + if (inverted && type == 'column') { + typeClass = seriesTypes.bar; + } else if (!inverted && type == 'bar') { + typeClass = seriesTypes.column; + } + } + + serie = new typeClass(); + + serie.init(chart, options); + + // set internal chart properties + if (!hasRendered && serie.inverted) { + inverted = true; + } + if (serie.isCartesian) { + hasCartesianSeries = serie.isCartesian; + } + + series.push(serie); + + return serie; + } + + /** + * Add a series dynamically after time + * + * @param {Object} options The config options + * @param {Boolean} redraw Whether to redraw the chart after adding. Defaults to true. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + * @return {Object} series The newly created series object + */ + function addSeries(options, redraw, animation) { + var series; + + if (options) { + setAnimation(animation, chart); + redraw = pick(redraw, true); // defaults to true + + fireEvent(chart, 'addSeries', { options: options }, function() { + series = initSeries(options); + series.isDirty = true; + + chart.isDirtyLegend = true; // the series array is out of sync with the display + if (redraw) { + chart.redraw(); + } + }); + } + + return series; + } + + /** + * Check whether a given point is within the plot area + * + * @param {Number} x Pixel x relative to the coordinateSystem + * @param {Number} y Pixel y relative to the coordinateSystem + */ + isInsidePlot = function(x, y) { + return x >= 0 && + x <= plotWidth && + y >= 0 && + y <= plotHeight; + }; + + /** + * Adjust all axes tick amounts + */ + function adjustTickAmounts() { + if (optionsChart.alignTicks !== false) { + each(axes, function(axis) { + axis.adjustTickAmount(); + }); + } + maxTicks = null; + } + + /** + * Redraw legend, axes or series based on updated data + * + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + function redraw(animation) { + var redrawLegend = chart.isDirtyLegend, + hasStackedSeries, + isDirtyBox = chart.isDirtyBox, // todo: check if it has actually changed? + seriesLength = series.length, + i = seriesLength, + clipRect = chart.clipRect, + serie; + + setAnimation(animation, chart); + + // link stacked series + while (i--) { + serie = series[i]; + if (serie.isDirty && serie.options.stacking) { + hasStackedSeries = true; + break; + } + } + if (hasStackedSeries) { // mark others as dirty + i = seriesLength; + while (i--) { + serie = series[i]; + if (serie.options.stacking) { + serie.isDirty = true; + } + } + } + + // handle updated data in the series + each(series, function(serie) { + if (serie.isDirty) { // prepare the data so axis can read it + serie.cleanData(); + serie.getSegments(); + + if (serie.options.legendType == 'point') { + redrawLegend = true; + } + } + }); + + // handle added or removed series + if (redrawLegend && legend.renderLegend) { // series or pie points are added or removed + // draw legend graphics + legend.renderLegend(); + + chart.isDirtyLegend = false; + } + + if (hasCartesianSeries) { + if (!isResizing) { + + // reset maxTicks + maxTicks = null; + + // set axes scales + each(axes, function(axis) { + axis.setScale(); + }); + } + adjustTickAmounts(); + getMargins(); + + // redraw axes + each(axes, function(axis) { + if (axis.isDirty || isDirtyBox) { + axis.redraw(); + isDirtyBox = true; // always redraw box to reflect changes in the axis labels + } + }); + + + } + + // the plot areas size has changed + if (isDirtyBox) { + drawChartBox(); + placeTrackerGroup(); + + // move clip rect + if (clipRect) { + stop(clipRect); + clipRect.animate({ // for chart resize + width: chart.plotSizeX, + height: chart.plotSizeY + }); + } + + } + + + // redraw affected series + each(series, function(serie) { + if (serie.isDirty && serie.visible && + (!serie.isCartesian || serie.xAxis)) { // issue #153 + serie.redraw(); + } + }); + + + // hide tooltip and hover states + if (tracker && tracker.resetTracker) { + tracker.resetTracker(); + } + + // fire the event + fireEvent(chart, 'redraw'); + } + + + + /** + * Dim the chart and show a loading text or symbol + * @param {String} str An optional text to show in the loading label instead of the default one + */ + function showLoading(str) { + var loadingOptions = options.loading; + + // create the layer at the first call + if (!loadingDiv) { + loadingDiv = createElement(DIV, { + className: 'highcharts-loading' + }, extend(loadingOptions.style, { + left: plotLeft + PX, + top: plotTop + PX, + width: plotWidth + PX, + height: plotHeight + PX, + zIndex: 10, + display: NONE + }), container); + + loadingSpan = createElement( + 'span', + null, + loadingOptions.labelStyle, + loadingDiv + ); + + } + + // update text + loadingSpan.innerHTML = str || options.lang.loading; + + // show it + if (!loadingShown) { + css(loadingDiv, { opacity: 0, display: '' }); + animate(loadingDiv, { + opacity: loadingOptions.style.opacity + }, { + duration: loadingOptions.showDuration + }); + loadingShown = true; + } + } + /** + * Hide the loading layer + */ + function hideLoading() { + animate(loadingDiv, { + opacity: 0 + }, { + duration: options.loading.hideDuration, + complete: function() { + css(loadingDiv, { display: NONE }); + } + }); + loadingShown = false; + } + + /** + * Get an axis, series or point object by id. + * @param id {String} The id as given in the configuration options + */ + function get(id) { + var i, + j, + data; + + // search axes + for (i = 0; i < axes.length; i++) { + if (axes[i].options.id == id) { + return axes[i]; + } + } + + // search series + for (i = 0; i < series.length; i++) { + if (series[i].options.id == id) { + return series[i]; + } + } + + // search points + for (i = 0; i < series.length; i++) { + data = series[i].data; + for (j = 0; j < data.length; j++) { + if (data[j].id == id) { + return data[j]; + } + } + } + return null; + } + + /** + * Create the Axis instances based on the config options + */ + function getAxes() { + var xAxisOptions = options.xAxis || {}, + yAxisOptions = options.yAxis || {}, + axis; + + // make sure the options are arrays and add some members + xAxisOptions = splat(xAxisOptions); + each(xAxisOptions, function(axis, i) { + axis.index = i; + axis.isX = true; + }); + + yAxisOptions = splat(yAxisOptions); + each(yAxisOptions, function(axis, i) { + axis.index = i; + }); + + // concatenate all axis options into one array + axes = xAxisOptions.concat(yAxisOptions); + + // loop the options and construct axis objects + chart.xAxis = []; + chart.yAxis = []; + axes = map(axes, function(axisOptions) { + axis = new Axis(chart, axisOptions); + chart[axis.isXAxis ? 'xAxis' : 'yAxis'].push(axis); + + return axis; + }); + + adjustTickAmounts(); + } + + + /** + * Get the currently selected points from all series + */ + function getSelectedPoints() { + var points = []; + each(series, function(serie) { + points = points.concat( grep( serie.data, function(point) { + return point.selected; + })); + }); + return points; + } + + /** + * Get the currently selected series + */ + function getSelectedSeries() { + return grep(series, function (serie) { + return serie.selected; + }); + } + + /** + * Zoom out to 1:1 + */ + zoomOut = function () { + fireEvent(chart, 'selection', { resetSelection: true }, zoom); + chart.toolbar.remove('zoom'); + + }; + /** + * Zoom into a given portion of the chart given by axis coordinates + * @param {Object} event + */ + zoom = function (event) { + + // add button to reset selection + var lang = defaultOptions.lang, + animate = chart.pointCount < 100; + chart.toolbar.add('zoom', lang.resetZoom, lang.resetZoomTitle, zoomOut); + + // if zoom is called with no arguments, reset the axes + if (!event || event.resetSelection) { + each(axes, function(axis) { + axis.setExtremes(null, null, false, animate); + }); + } + + // else, zoom in on all axes + else { + each(event.xAxis.concat(event.yAxis), function(axisData) { + var axis = axisData.axis; + + // don't zoom more than maxZoom + if (chart.tracker[axis.isXAxis ? 'zoomX' : 'zoomY']) { + axis.setExtremes(axisData.min, axisData.max, false, animate); + } + }); + } + + // redraw chart + redraw(); + }; + + /** + * Show the title and subtitle of the chart + * + * @param titleOptions {Object} New title options + * @param subtitleOptions {Object} New subtitle options + * + */ + function setTitle (titleOptions, subtitleOptions) { + + chartTitleOptions = merge(options.title, titleOptions); + chartSubtitleOptions = merge(options.subtitle, subtitleOptions); + + // add title and subtitle + each([ + ['title', titleOptions, chartTitleOptions], + ['subtitle', subtitleOptions, chartSubtitleOptions] + ], function(arr) { + var name = arr[0], + title = chart[name], + titleOptions = arr[1], + chartTitleOptions = arr[2]; + + if (title && titleOptions) { + title.destroy(); // remove old + title = null; + } + if (chartTitleOptions && chartTitleOptions.text && !title) { + chart[name] = renderer.text( + chartTitleOptions.text, + 0, + 0 + ) + .attr({ + align: chartTitleOptions.align, + 'class': 'highcharts-'+ name, + zIndex: 1 + }) + .css(chartTitleOptions.style) + .add() + .align(chartTitleOptions, false, spacingBox); + } + }); + + } + + /** + * Get chart width and height according to options and container size + */ + function getChartSize() { + + containerWidth = (renderToClone || renderTo).offsetWidth; + containerHeight = (renderToClone || renderTo).offsetHeight; + chart.chartWidth = chartWidth = optionsChart.width || containerWidth || 600; + chart.chartHeight = chartHeight = optionsChart.height || + // the offsetHeight of an empty container is 0 in standard browsers, but 19 in IE7: + (containerHeight > 19 ? containerHeight : 400); + } + + + /** + * Get the containing element, determine the size and create the inner container + * div to hold the chart + */ + function getContainer() { + renderTo = optionsChart.renderTo; + containerId = PREFIX + idCounter++; + + if (isString(renderTo)) { + renderTo = doc.getElementById(renderTo); + } + + // remove previous chart + renderTo.innerHTML = ''; + + // If the container doesn't have an offsetWidth, it has or is a child of a node + // that has display:none. We need to temporarily move it out to a visible + // state to determine the size, else the legend and tooltips won't render + // properly + if (!renderTo.offsetWidth) { + renderToClone = renderTo.cloneNode(0); + css(renderToClone, { + position: ABSOLUTE, + top: '-9999px', + display: '' + }); + doc.body.appendChild(renderToClone); + } + + // get the width and height + getChartSize(); + + // create the inner container + chart.container = container = createElement(DIV, { + className: 'highcharts-container' + + (optionsChart.className ? ' '+ optionsChart.className : ''), + id: containerId + }, extend({ + position: RELATIVE, + overflow: HIDDEN, // needed for context menu (avoid scrollbars) and + // content overflow in IE + width: chartWidth + PX, + height: chartHeight + PX, + textAlign: 'left' + }, optionsChart.style), + renderToClone || renderTo + ); + + chart.renderer = renderer = + optionsChart.forExport ? // force SVG, used for SVG export + new SVGRenderer(container, chartWidth, chartHeight, true) : + new Renderer(container, chartWidth, chartHeight); + + // Issue 110 workaround: + // In Firefox, if a div is positioned by percentage, its pixel position may land + // between pixels. The container itself doesn't display this, but an SVG element + // inside this container will be drawn at subpixel precision. In order to draw + // sharp lines, this must be compensated for. This doesn't seem to work inside + // iframes though (like in jsFiddle). + var subPixelFix, rect; + if (isFirefox && container.getBoundingClientRect) { + subPixelFix = function() { + css(container, { left: 0, top: 0 }); + rect = container.getBoundingClientRect(); + css(container, { + left: (-rect.left % 1) + PX, + top: (-rect.top % 1) + PX + }); + }; + + // run the fix now + subPixelFix(); + + // run it on resize + addEvent(win, 'resize', subPixelFix); + + // remove it on chart destroy + addEvent(chart, 'destroy', function() { + removeEvent(win, 'resize', subPixelFix); + }); + } + } + + /** + * Calculate margins by rendering axis labels in a preliminary position. Title, + * subtitle and legend have already been rendered at this stage, but will be + * moved into their final positions + */ + getMargins = function() { + var legendOptions = options.legend, + legendMargin = pick(legendOptions.margin, 10), + legendX = legendOptions.x, + legendY = legendOptions.y, + align = legendOptions.align, + verticalAlign = legendOptions.verticalAlign, + titleOffset; + + resetMargins(); + + // adjust for title and subtitle + if ((chart.title || chart.subtitle) && !defined(optionsMarginTop)) { + titleOffset = mathMax( + chart.title && !chartTitleOptions.floating && !chartTitleOptions.verticalAlign && chartTitleOptions.y || 0, + chart.subtitle && !chartSubtitleOptions.floating && !chartSubtitleOptions.verticalAlign && chartSubtitleOptions.y || 0 + ); + if (titleOffset) { + plotTop = mathMax(plotTop, titleOffset + pick(chartTitleOptions.margin, 15) + spacingTop); + } + } + // adjust for legend + if (legendOptions.enabled && !legendOptions.floating) { + if (align == 'right') { // horizontal alignment handled first + if (!defined(optionsMarginRight)) { + marginRight = mathMax( + marginRight, + legendWidth - legendX + legendMargin + spacingRight + ); + } + } else if (align == 'left') { + if (!defined(optionsMarginLeft)) { + plotLeft = mathMax( + plotLeft, + legendWidth + legendX + legendMargin + spacingLeft + ); + } + + } else if (verticalAlign == 'top') { + if (!defined(optionsMarginTop)) { + plotTop = mathMax( + plotTop, + legendHeight + legendY + legendMargin + spacingTop + ); + } + + } else if (verticalAlign == 'bottom') { + if (!defined(optionsMarginBottom)) { + marginBottom = mathMax( + marginBottom, + legendHeight - legendY + legendMargin + spacingBottom + ); + } + } + } + + // pre-render axes to get labels offset width + if (hasCartesianSeries) { + each(axes, function(axis) { + axis.getOffset(); + }); + } + + if (!defined(optionsMarginLeft)) { + plotLeft += axisOffset[3]; + } + if (!defined(optionsMarginTop)) { + plotTop += axisOffset[0]; + } + if (!defined(optionsMarginBottom)) { + marginBottom += axisOffset[2]; + } + if (!defined(optionsMarginRight)) { + marginRight += axisOffset[1]; + } + + setChartSize(); + + }; + + /** + * Add the event handlers necessary for auto resizing + * + */ + function initReflow() { + var reflowTimeout; + function reflow() { + var width = optionsChart.width || renderTo.offsetWidth, + height = optionsChart.height || renderTo.offsetHeight; + + if (width && height) { // means container is display:none + if (width != containerWidth || height != containerHeight) { + clearTimeout(reflowTimeout); + reflowTimeout = setTimeout(function() { + resize(width, height, false); + }, 100); + } + containerWidth = width; + containerHeight = height; + } + } + addEvent(window, 'resize', reflow); + addEvent(chart, 'destroy', function() { + removeEvent(window, 'resize', reflow); + }); + } + + /** + * Resize the chart to a given width and height + * @param {Number} width + * @param {Number} height + * @param {Object|Boolean} animation + */ + resize = function(width, height, animation) { + var chartTitle = chart.title, + chartSubtitle = chart.subtitle; + + isResizing += 1; + + // set the animation for the current process + setAnimation(animation, chart); + + oldChartHeight = chartHeight; + oldChartWidth = chartWidth; + chartWidth = mathRound(width); + chartHeight = mathRound(height); + + css(container, { + width: chartWidth + PX, + height: chartHeight + PX + }); + renderer.setSize(chartWidth, chartHeight, animation); + + // update axis lengths for more correct tick intervals: + plotWidth = chartWidth - plotLeft - marginRight; + plotHeight = chartHeight - plotTop - marginBottom; + + // handle axes + maxTicks = null; + each(axes, function(axis) { + axis.isDirty = true; + axis.setScale(); + }); + + // make sure non-cartesian series are also handled + each(series, function(serie) { + serie.isDirty = true; + }); + + chart.isDirtyLegend = true; // force legend redraw + chart.isDirtyBox = true; // force redraw of plot and chart border + + getMargins(); + + // move titles + if (chartTitle) { + chartTitle.align(null, null, spacingBox); + } + if (chartSubtitle) { + chartSubtitle.align(null, null, spacingBox); + } + + redraw(animation); + + + oldChartHeight = null; + fireEvent(chart, 'resize'); + + // fire endResize and set isResizing back + setTimeout(function() { + fireEvent(chart, 'endResize', null, function() { + isResizing -= 1; + }); + }, globalAnimation && globalAnimation.duration || 500); + }; + + /** + * Set the public chart properties. This is done before and after the pre-render + * to determine margin sizes + */ + setChartSize = function() { + + chart.plotLeft = plotLeft = mathRound(plotLeft); + chart.plotTop = plotTop = mathRound(plotTop); + chart.plotWidth = plotWidth = mathRound(chartWidth - plotLeft - marginRight); + chart.plotHeight = plotHeight = mathRound(chartHeight - plotTop - marginBottom); + + chart.plotSizeX = inverted ? plotHeight : plotWidth; + chart.plotSizeY = inverted ? plotWidth : plotHeight; + + spacingBox = { + x: spacingLeft, + y: spacingTop, + width: chartWidth - spacingLeft - spacingRight, + height: chartHeight - spacingTop - spacingBottom + }; + }; + + /** + * Initial margins before auto size margins are applied + */ + resetMargins = function() { + plotTop = pick(optionsMarginTop, spacingTop); + marginRight = pick(optionsMarginRight, spacingRight); + marginBottom = pick(optionsMarginBottom, spacingBottom); + plotLeft = pick(optionsMarginLeft, spacingLeft); + axisOffset = [0, 0, 0, 0]; // top, right, bottom, left + }; + + /** + * Draw the borders and backgrounds for chart and plot area + */ + drawChartBox = function() { + var chartBorderWidth = optionsChart.borderWidth || 0, + chartBackgroundColor = optionsChart.backgroundColor, + plotBackgroundColor = optionsChart.plotBackgroundColor, + plotBackgroundImage = optionsChart.plotBackgroundImage, + mgn, + plotSize = { + x: plotLeft, + y: plotTop, + width: plotWidth, + height: plotHeight + }; + + // Chart area + mgn = chartBorderWidth + (optionsChart.shadow ? 8 : 0); + + if (chartBorderWidth || chartBackgroundColor) { + if (!chartBackground) { + chartBackground = renderer.rect(mgn / 2, mgn / 2, chartWidth - mgn, chartHeight - mgn, + optionsChart.borderRadius, chartBorderWidth) + .attr({ + stroke: optionsChart.borderColor, + 'stroke-width': chartBorderWidth, + fill: chartBackgroundColor || NONE + }) + .add() + .shadow(optionsChart.shadow); + } else { // resize + chartBackground.animate( + chartBackground.crisp(null, null, null, chartWidth - mgn, chartHeight - mgn) + ); + } + } + + + // Plot background + if (plotBackgroundColor) { + if (!plotBackground) { + plotBackground = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0) + .attr({ + fill: plotBackgroundColor + }) + .add() + .shadow(optionsChart.plotShadow); + } else { + plotBackground.animate(plotSize); + } + } + if (plotBackgroundImage) { + if (!plotBGImage) { + plotBGImage = renderer.image(plotBackgroundImage, plotLeft, plotTop, plotWidth, plotHeight) + .add(); + } else { + plotBGImage.animate(plotSize); + } + } + + // Plot area border + if (optionsChart.plotBorderWidth) { + if (!plotBorder) { + plotBorder = renderer.rect(plotLeft, plotTop, plotWidth, plotHeight, 0, optionsChart.plotBorderWidth) + .attr({ + stroke: optionsChart.plotBorderColor, + 'stroke-width': optionsChart.plotBorderWidth, + zIndex: 4 + }) + .add(); + } else { + plotBorder.animate( + plotBorder.crisp(null, plotLeft, plotTop, plotWidth, plotHeight) + ); + } + } + + // reset + chart.isDirtyBox = false; + }; + + /** + * Render all graphics for the chart + */ + function render () { + var labels = options.labels, + credits = options.credits, + creditsHref; + + // Title + setTitle(); + + + // Legend + legend = chart.legend = new Legend(chart); + + // Get margins by pre-rendering axes + getMargins(); + each(axes, function(axis) { + axis.setTickPositions(true); // update to reflect the new margins + }); + adjustTickAmounts(); + getMargins(); // second pass to check for new labels + + + // Draw the borders and backgrounds + drawChartBox(); + + // Axes + if (hasCartesianSeries) { + each(axes, function(axis) { + axis.render(); + }); + } + + + // The series + if (!chart.seriesGroup) { + chart.seriesGroup = renderer.g('series-group') + .attr({ zIndex: 3 }) + .add(); + } + each(series, function(serie) { + serie.translate(); + serie.setTooltipPoints(); + serie.render(); + }); + + + // Labels + if (labels.items) { + each(labels.items, function() { + var style = extend(labels.style, this.style), + x = pInt(style.left) + plotLeft, + y = pInt(style.top) + plotTop + 12; + + // delete to prevent rewriting in IE + delete style.left; + delete style.top; + + renderer.text( + this.html, + x, + y + ) + .attr({ zIndex: 2 }) + .css(style) + .add(); + + }); + } + + // Toolbar (don't redraw) + if (!chart.toolbar) { + chart.toolbar = Toolbar(chart); + } + + // Credits + if (credits.enabled && !chart.credits) { + creditsHref = credits.href; + renderer.text( + credits.text, + 0, + 0 + ) + .on('click', function() { + if (creditsHref) { + location.href = creditsHref; + } + }) + .attr({ + align: credits.position.align, + zIndex: 8 + }) + .css(credits.style) + .add() + .align(credits.position); + } + + placeTrackerGroup(); + + // Set flag + chart.hasRendered = true; + + // If the chart was rendered outside the top container, put it back in + if (renderToClone) { + renderTo.appendChild(container); + discardElement(renderToClone); + //updatePosition(container); + } + } + + /** + * Clean up memory usage + */ + function destroy() { + var i = series.length, + parentNode = container && container.parentNode; + + // fire the chart.destoy event + fireEvent(chart, 'destroy'); + + // remove events + removeEvent(win, 'unload', destroy); + removeEvent(chart); + + each(axes, function(axis) { + removeEvent(axis); + }); + + // destroy each series + while (i--) { + series[i].destroy(); + } + + // remove container and all SVG + if (container) { // can break in IE when destroyed before finished loading + container.innerHTML = ''; + removeEvent(container); + if (parentNode) { + parentNode.removeChild(container); + } + + // IE6 leak + container = null; + } + + // IE7 leak + if (renderer) { // can break in IE when destroyed before finished loading + renderer.alignedObjects = null; + } + + // memory and CPU leak + clearInterval(tooltipInterval); + + // clean it all up + for (i in chart) { + delete chart[i]; + } + + } + /** + * Prepare for first rendering after all data are loaded + */ + function firstRender() { + + // VML namespaces can't be added until after complete. Listening + // for Perini's doScroll hack is not enough. + var onreadystatechange = 'onreadystatechange'; + if (!hasSVG && win == win.top && doc.readyState != 'complete') { + doc.attachEvent(onreadystatechange, function() { + doc.detachEvent(onreadystatechange, firstRender); + firstRender(); + }); + return; + } + + // create the container + getContainer(); + + resetMargins(); + setChartSize(); + + // Initialize the series + each(options.series || [], function(serieOptions) { + initSeries(serieOptions); + }); + + // Set the common inversion and transformation for inverted series after initSeries + chart.inverted = inverted = pick(inverted, options.chart.inverted); + + + getAxes(); + + + chart.render = render; + + // depends on inverted and on margins being set + chart.tracker = tracker = new MouseTracker(chart, options.tooltip); + + //globalAnimation = false; + render(); + + fireEvent(chart, 'load'); + + //globalAnimation = true; + + // run callbacks + if (callback) { + callback.apply(chart, [chart]); + } + each(chart.callbacks, function(fn) { + fn.apply(chart, [chart]); + }); + } + + // Run chart + + + + // Set to zero for each new chart + colorCounter = 0; + symbolCounter = 0; + + // Destroy the chart and free up memory. + addEvent(win, 'unload', destroy); + + // Set up auto resize + if (optionsChart.reflow !== false) { + addEvent(chart, 'load', initReflow); + } + + // Chart event handlers + if (chartEvents) { + for (eventType in chartEvents) { + addEvent(chart, eventType, chartEvents[eventType]); + } + } + + + chart.options = options; + chart.series = series; + + + + + + + // Expose methods and variables + chart.addSeries = addSeries; + chart.animation = pick(optionsChart.animation, true); + chart.destroy = destroy; + chart.get = get; + chart.getSelectedPoints = getSelectedPoints; + chart.getSelectedSeries = getSelectedSeries; + chart.hideLoading = hideLoading; + chart.isInsidePlot = isInsidePlot; + chart.redraw = redraw; + chart.setSize = resize; + chart.setTitle = setTitle; + chart.showLoading = showLoading; + chart.pointCount = 0; + /* + if ($) $(function() { + $container = $('#container'); + var origChartWidth, + origChartHeight; + if ($container) { + $('<button>+</button>') + .insertBefore($container) + .click(function() { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(chartWidth *= 1.1, chartHeight *= 1.1); + }); + $('<button>-</button>') + .insertBefore($container) + .click(function() { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(chartWidth *= 0.9, chartHeight *= 0.9); + }); + $('<button>1:1</button>') + .insertBefore($container) + .click(function() { + if (origChartWidth === UNDEFINED) { + origChartWidth = chartWidth; + origChartHeight = chartHeight; + } + chart.resize(origChartWidth, origChartHeight); + }); + } + }) + */ + + + + + firstRender(); + + +} // end Chart + +// Hook for exporting module +Chart.prototype.callbacks = []; + +/** + * The Point object and prototype. Inheritable and used as base for PiePoint + */ +var Point = function() {}; +Point.prototype = { + + /** + * Initialize the point + * @param {Object} series The series object containing this point + * @param {Object} options The data in either number, array or object format + */ + init: function(series, options) { + var point = this, + defaultColors; + point.series = series; + point.applyOptions(options); + point.pointAttr = {}; + + if (series.options.colorByPoint) { + defaultColors = series.chart.options.colors; + if (!point.options) { + point.options = {}; + } + point.color = point.options.color = point.color || defaultColors[colorCounter++]; + + // loop back to zero + if (colorCounter >= defaultColors.length) { + colorCounter = 0; + } + } + + series.chart.pointCount++; + return point; + }, + /** + * Apply the options containing the x and y data and possible some extra properties. + * This is called on point init or from point.update. + * + * @param {Object} options + */ + applyOptions: function(options) { + var point = this, + series = point.series; + + point.config = options; + + // onedimensional array input + if (isNumber(options) || options === null) { + point.y = options; + } + + // object input + else if (isObject(options) && !isNumber(options.length)) { + + // copy options directly to point + extend(point, options); + point.options = options; + } + + // categorized data with name in first position + else if (isString(options[0])) { + point.name = options[0]; + point.y = options[1]; + } + + // two-dimentional array + else if (isNumber(options[0])) { + point.x = options[0]; + point.y = options[1]; + } + + /* + * If no x is set by now, get auto incremented value. All points must have an + * x value, however the y value can be null to create a gap in the series + */ + if (point.x === UNDEFINED) { + point.x = series.autoIncrement(); + } + + }, + + /** + * Destroy a point to clear memory. Its reference still stays in series.data. + */ + destroy: function() { + var point = this, + series = point.series, + prop; + + series.chart.pointCount--; + + if (point == series.chart.hoverPoint) { + point.onMouseOut(); + } + series.chart.hoverPoints = null; // remove reference + + // remove all events + removeEvent(point); + + each(['graphic', 'tracker', 'group', 'dataLabel', 'connector'], function(prop) { + if (point[prop]) { + point[prop].destroy(); + } + }); + + if (point.legendItem) { // pies have legend items + point.series.chart.legend.destroyItem(point); + } + + for (prop in point) { + point[prop] = null; + } + + + }, + + /** + * Toggle the selection status of a point + * @param {Boolean} selected Whether to select or unselect the point. + * @param {Boolean} accumulate Whether to add to the previous selection. By default, + * this happens if the control key (Cmd on Mac) was pressed during clicking. + */ + select: function(selected, accumulate) { + var point = this, + series = point.series, + chart = series.chart; + + point.selected = selected = pick(selected, !point.selected); + + //series.isDirty = true; + point.firePointEvent(selected ? 'select' : 'unselect'); + point.setState(selected && SELECT_STATE); + + // unselect all other points unless Ctrl or Cmd + click + if (!accumulate) { + each(chart.getSelectedPoints(), function (loopPoint) { + if (loopPoint.selected && loopPoint != point) { + loopPoint.selected = false; + loopPoint.setState(NORMAL_STATE); + loopPoint.firePointEvent('unselect'); + } + }); + } + + }, + + onMouseOver: function() { + var point = this, + chart = point.series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // set normal state to previous series + if (hoverPoint && hoverPoint != point) { + hoverPoint.onMouseOut(); + } + + // trigger the event + point.firePointEvent('mouseOver'); + + // update the tooltip + if (tooltip && !tooltip.shared) { + tooltip.refresh(point); + } + + // hover this + point.setState(HOVER_STATE); + chart.hoverPoint = point; + }, + + onMouseOut: function() { + var point = this; + point.firePointEvent('mouseOut'); + + point.setState(); + point.series.chart.hoverPoint = null; + }, + + /** + * Extendable method for formatting each point's tooltip line + * + * @param {Boolean} useHeader Whether a common header is used for multiple series in the tooltip + * + * @return {String} A string to be concatenated in to the common tooltip text + */ + tooltipFormatter: function(useHeader) { + var point = this, + series = point.series; + + return ['<span style="color:'+ series.color +'">', (point.name || series.name), '</span>: ', + (!useHeader ? ('<b>x = '+ (point.name || point.x) + ',</b> ') : ''), + '<b>', (!useHeader ? 'y = ' : '' ), point.y, '</b><br/>'].join(''); + + }, + + /** + * Get the formatted text for this point's data label + * + * @return {String} The formatted data label pseudo-HTML + */ + getDataLabelText: function() { + var point = this; + return this.series.options.dataLabels.formatter.call({ + x: point.x, + y: point.y, + series: point.series, + point: point, + percentage: point.percentage, + total: point.total || point.stackTotal + }); + }, + + /** + * Update the point with new options (typically x/y data) and optionally redraw the series. + * + * @param {Object} options Point options as defined in the series.data array + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + * + */ + update: function(options, redraw, animation) { + var point = this, + series = point.series, + dataLabel = point.dataLabel, + graphic = point.graphic, + chart = series.chart; + + redraw = pick(redraw, true); + + // fire the event with a default handler of doing the update + point.firePointEvent('update', { options: options }, function() { + + point.applyOptions(options); + + if (dataLabel) { + dataLabel.attr({ + text: point.getDataLabelText() + }) + } + + // update visuals + if (isObject(options)) { + series.getAttribs(); + if (graphic) { + graphic.attr(point.pointAttr[series.state]); + } + } + + // redraw + series.isDirty = true; + if (redraw) { + chart.redraw(animation); + } + }); + }, + + /** + * Remove a point and optionally redraw the series and if necessary the axes + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + remove: function(redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + data = series.data; + + setAnimation(animation, chart); + redraw = pick(redraw, true); + + // fire the event with a default handler of removing the point + point.firePointEvent('remove', null, function() { + + erase(data, point); + + point.destroy(); + + + // redraw + series.isDirty = true; + if (redraw) { + chart.redraw(); + } + }); + + + }, + + /** + * Fire an event on the Point object. Must not be renamed to fireEvent, as this + * causes a name clash in MooTools + * @param {String} eventType + * @param {Object} eventArgs Additional event arguments + * @param {Function} defaultFunction Default event handler + */ + firePointEvent: function(eventType, eventArgs, defaultFunction) { + var point = this, + series = this.series, + seriesOptions = series.options; + + // load event handlers on demand to save time on mouseover/out + if (seriesOptions.point.events[eventType] || ( + point.options && point.options.events && point.options.events[eventType])) { + this.importEvents(); + } + + // add default handler if in selection mode + if (eventType == 'click' && seriesOptions.allowPointSelect) { + defaultFunction = function (event) { + // Control key is for Windows, meta (= Cmd key) for Mac, Shift for Opera + point.select(null, event.ctrlKey || event.metaKey || event.shiftKey); + }; + } + + fireEvent(this, eventType, eventArgs, defaultFunction); + }, + /** + * Import events from the series' and point's options. Only do it on + * demand, to save processing time on hovering. + */ + importEvents: function() { + if (!this.hasImportedEvents) { + var point = this, + options = merge(point.series.options.point, point.options), + events = options.events, + eventType; + + point.events = events; + + for (eventType in events) { + addEvent(point, eventType, events[eventType]); + } + this.hasImportedEvents = true; + + } + }, + + /** + * Set the point's state + * @param {String} state + */ + setState: function(state) { + var point = this, + series = point.series, + stateOptions = series.options.states, + markerOptions = defaultPlotOptions[series.type].marker && series.options.marker, + normalDisabled = markerOptions && !markerOptions.enabled, + markerStateOptions = markerOptions && markerOptions.states[state], + stateDisabled = markerStateOptions && markerStateOptions.enabled === false, + stateMarkerGraphic = series.stateMarkerGraphic, + chart = series.chart, + pointAttr = point.pointAttr; + + if (!state) { + state = NORMAL_STATE; // empty string + } + + if ( + // already has this state + state == point.state || + // selected points don't respond to hover + (point.selected && state != SELECT_STATE) || + // series' state options is disabled + (stateOptions[state] && stateOptions[state].enabled === false) || + // point marker's state options is disabled + (state && (stateDisabled || normalDisabled && !markerStateOptions.enabled)) + + ) { + return; + } + + // apply hover styles to the existing point + if (point.graphic) { + point.graphic.attr(pointAttr[state]); + } + // if a graphic is not applied to each point in the normal state, create a shared + // graphic for the hover state + else { + if (state) { + if (!stateMarkerGraphic) { + series.stateMarkerGraphic = stateMarkerGraphic = chart.renderer.circle( + 0, 0, pointAttr[state].r + ) + .attr(pointAttr[state]) + .add(series.group); + } + + stateMarkerGraphic.translate( + point.plotX, + point.plotY + ); + } + + if (stateMarkerGraphic) { + stateMarkerGraphic[state ? 'show' : 'hide'](); + } + } + + point.state = state; + } +}; + +/** + * The base function which all other series types inherit from + * @param {Object} chart + * @param {Object} options + */ +var Series = function() {}; + +Series.prototype = { + + isCartesian: true, + type: 'line', + pointClass: Point, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'lineColor', + 'stroke-width': 'lineWidth', + fill: 'fillColor', + r: 'radius' + }, + init: function(chart, options) { + var series = this, + eventType, + events, + //pointEvent, + index = chart.series.length; + + series.chart = chart; + options = series.setOptions(options); // merge with plotOptions + + // set some variables + extend(series, { + index: index, + options: options, + name: options.name || 'Series '+ (index + 1), + state: NORMAL_STATE, + pointAttr: {}, + visible: options.visible !== false, // true by default + selected: options.selected === true // false by default + }); + + // register event listeners + events = options.events; + for (eventType in events) { + addEvent(series, eventType, events[eventType]); + } + if ( + (events && events.click) || + (options.point && options.point.events && options.point.events.click) || + options.allowPointSelect + ) { + chart.runTrackerClick = true; + } + + series.getColor(); + series.getSymbol(); + + // set the data + series.setData(options.data, false); + + }, + + + /** + * Return an auto incremented x value based on the pointStart and pointInterval options. + * This is only used if an x value is not given for the point that calls autoIncrement. + */ + autoIncrement: function() { + var series = this, + options = series.options, + xIncrement = series.xIncrement; + + xIncrement = pick(xIncrement, options.pointStart, 0); + + series.pointInterval = pick(series.pointInterval, options.pointInterval, 1); + + series.xIncrement = xIncrement + series.pointInterval; + return xIncrement; + }, + + /** + * Sort the data and remove duplicates + */ + cleanData: function() { + var series = this, + chart = series.chart, + data = series.data, + closestPoints, + smallestInterval, + chartSmallestInterval = chart.smallestInterval, + interval, + i; + + // sort the data points + data.sort(function(a, b){ + return (a.x - b.x); + }); + + // remove points with equal x values + // record the closest distance for calculation of column widths + for (i = data.length - 1; i >= 0; i--) { + if (data[i - 1]) { + if (data[i - 1].x == data[i].x) { + data.splice(i - 1, 1); // remove the duplicate + } + + } + } + + + // find the closes pair of points + for (i = data.length - 1; i >= 0; i--) { + if (data[i - 1]) { + interval = data[i].x - data[i - 1].x; + if (smallestInterval === UNDEFINED || interval < smallestInterval) { + smallestInterval = interval; + closestPoints = i; + } + } + } + + if (chartSmallestInterval === UNDEFINED || smallestInterval < chartSmallestInterval) { + chart.smallestInterval = smallestInterval; + } + series.closestPoints = closestPoints; + }, + + /** + * Divide the series data into segments divided by null values. Also sort + * the data points and delete duplicate values. + */ + getSegments: function() { + var lastNull = -1, + segments = [], + data = this.data; + + // create the segments + each(data, function(point, i) { + if (point.y === null) { + if (i > lastNull + 1) { + segments.push(data.slice(lastNull + 1, i)); + } + lastNull = i; + } else if (i == data.length - 1) { // last value + segments.push(data.slice(lastNull + 1, i + 1)); + } + }); + this.segments = segments; + + + }, + /** + * Set the series options by merging from the options tree + * @param {Object} itemOptions + */ + setOptions: function(itemOptions) { + var plotOptions = this.chart.options.plotOptions, + options = merge( + plotOptions[this.type], + plotOptions.series, + itemOptions + ); + + return options; + + }, + /** + * Get the series' color + */ + getColor: function(){ + var defaultColors = this.chart.options.colors; + this.color = this.options.color || defaultColors[colorCounter++] || '#0000ff'; + if (colorCounter >= defaultColors.length) { + colorCounter = 0; + } + }, + /** + * Get the series' symbol + */ + getSymbol: function(){ + var defaultSymbols = this.chart.options.symbols, + symbol = this.options.marker.symbol || defaultSymbols[symbolCounter++]; + this.symbol = symbol; + if (symbolCounter >= defaultSymbols.length) { + symbolCounter = 0; + } + }, + + /** + * Add a point dynamically after chart load time + * @param {Object} options Point options as given in series.data + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean} shift If shift is true, a point is shifted off the start + * of the series as one is appended to the end. + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + addPoint: function(options, redraw, shift, animation) { + var series = this, + data = series.data, + graph = series.graph, + area = series.area, + chart = series.chart, + point = (new series.pointClass()).init(series, options); + + setAnimation(animation, chart); + + if (graph && shift) { // make graph animate sideways + graph.shift = shift; + } + if (area) { + area.shift = shift; + area.isArea = true; + } + + redraw = pick(redraw, true); + + data.push(point); + if (shift) { + data[0].remove(false); + } + + + // redraw + series.isDirty = true; + if (redraw) { + chart.redraw(); + } + }, + + /** + * Replace the series data with a new set of data + * @param {Object} data + * @param {Object} redraw + */ + setData: function(data, redraw) { + var series = this, + oldData = series.data, + initialColor = series.initialColor, + chart = series.chart, + i = oldData && oldData.length || 0; + + series.xIncrement = null; // reset for new data + if (defined(initialColor)) { // reset colors for pie + colorCounter = initialColor; + } + + data = map(splat(data || []), function(pointOptions) { + return (new series.pointClass()).init(series, pointOptions); + }); + + // destroy old points + while (i--) { + oldData[i].destroy(); + } + + // set the data + series.data = data; + + series.cleanData(); + series.getSegments(); + + // redraw + series.isDirty = true; + chart.isDirtyBox = true; + if (pick(redraw, true)) { + chart.redraw(false); + } + }, + + /** + * Remove a series and optionally redraw the chart + * + * @param {Boolean} redraw Whether to redraw the chart or wait for an explicit call + * @param {Boolean|Object} animation Whether to apply animation, and optionally animation + * configuration + */ + + remove: function(redraw, animation) { + var series = this, + chart = series.chart; + redraw = pick(redraw, true); + + if (!series.isRemoving) { /* prevent triggering native event in jQuery + (calling the remove function from the remove event) */ + series.isRemoving = true; + + // fire the event with a default handler of removing the point + fireEvent(series, 'remove', null, function() { + + + // destroy elements + series.destroy(); + + + // redraw + chart.isDirtyLegend = chart.isDirtyBox = true; + if (redraw) { + chart.redraw(animation); + } + }); + + } + series.isRemoving = false; + }, + + /** + * Translate data points from raw data values to chart specific positioning data + * needed later in drawPoints, drawGraph and drawTracker. + */ + translate: function() { + var series = this, + chart = series.chart, + stacking = series.options.stacking, + categories = series.xAxis.categories, + yAxis = series.yAxis, + data = series.data, + i = data.length; + + // do the translation + while (i--) { + var point = data[i], + xValue = point.x, + yValue = point.y, + yBottom = point.low, + stack = yAxis.stacks[(yValue < 0 ? '-' : '') + series.stackKey], + pointStack, + pointStackTotal; + point.plotX = series.xAxis.translate(xValue); + + // calculate the bottom y value for stacked series + if (stacking && series.visible && stack && stack[xValue]) { + pointStack = stack[xValue]; + pointStackTotal = pointStack.total; + pointStack.cum = yBottom = pointStack.cum - yValue; // start from top + yValue = yBottom + yValue; + + if (stacking == 'percent') { + yBottom = pointStackTotal ? yBottom * 100 / pointStackTotal : 0; + yValue = pointStackTotal ? yValue * 100 / pointStackTotal : 0; + } + + point.percentage = pointStackTotal ? point.y * 100 / pointStackTotal : 0; + point.stackTotal = pointStackTotal; + } + + if (defined(yBottom)) { + point.yBottom = yAxis.translate(yBottom, 0, 1); + } + + // set the y value + if (yValue !== null) { + point.plotY = yAxis.translate(yValue, 0, 1); + } + + // set client related positions for mouse tracking + point.clientX = chart.inverted ? + chart.plotHeight - point.plotX : + point.plotX; // for mouse tracking + + // some API data + point.category = categories && categories[point.x] !== UNDEFINED ? + categories[point.x] : point.x; + + } + }, + /** + * Memoize tooltip texts and positions + */ + setTooltipPoints: function (renew) { + var series = this, + chart = series.chart, + inverted = chart.inverted, + data = [], + plotSize = mathRound((inverted ? chart.plotTop : chart.plotLeft) + chart.plotSizeX), + low, + high, + tooltipPoints = []; // a lookup array for each pixel in the x dimension + + // renew + if (renew) { + series.tooltipPoints = null; + } + + // concat segments to overcome null values + each(series.segments, function(segment){ + data = data.concat(segment); + }); + + // loop the concatenated data and apply each point to all the closest + // pixel positions + if (series.xAxis && series.xAxis.reversed) { + data = data.reverse();//reverseArray(data); + } + + each(data, function(point, i) { + + low = data[i - 1] ? data[i - 1].high + 1 : 0; + high = point.high = data[i + 1] ? ( + mathFloor((point.plotX + (data[i + 1] ? + data[i + 1].plotX : plotSize)) / 2)) : + plotSize; + + while (low <= high) { + tooltipPoints[inverted ? plotSize - low++ : low++] = point; + } + }); + series.tooltipPoints = tooltipPoints; + }, + + + + + /** + * Series mouse over handler + */ + onMouseOver: function() { + var series = this, + chart = series.chart, + hoverSeries = chart.hoverSeries; + + if (!hasTouch && chart.mouseIsDown) { + return; + } + + // set normal state to previous series + if (hoverSeries && hoverSeries != series) { + hoverSeries.onMouseOut(); + } + + // trigger the event, but to save processing time, + // only if defined + if (series.options.events.mouseOver) { + fireEvent(series, 'mouseOver'); + } + + + // bring to front + // Todo: optimize. This is one of two operations slowing down the tooltip in Firefox. + // Can the tracking be done otherwise? + if (series.tracker) { + series.tracker.toFront(); + } + + // hover this + series.setState(HOVER_STATE); + chart.hoverSeries = series; + }, + + /** + * Series mouse out handler + */ + onMouseOut: function() { + // trigger the event only if listeners exist + var series = this, + options = series.options, + chart = series.chart, + tooltip = chart.tooltip, + hoverPoint = chart.hoverPoint; + + // trigger mouse out on the point, which must be in this series + if (hoverPoint) { + hoverPoint.onMouseOut(); + } + + // fire the mouse out event + if (series && options.events.mouseOut) { + fireEvent(series, 'mouseOut'); + } + + + // hide the tooltip + if (tooltip && !options.stickyTracking) { + tooltip.hide(); + } + + // set normal state + series.setState(); + chart.hoverSeries = null; + }, + + /** + * Animate in the series + */ + animate: function(init) { + var series = this, + chart = series.chart, + clipRect = series.clipRect, + animation = series.options.animation; + + if (animation && !isObject(animation)) { + animation = {}; + } + + if (init) { // initialize the animation + if (!clipRect.isAnimating) { // apply it only for one of the series + clipRect.attr( 'width', 0 ); + clipRect.isAnimating = true; + } + + } else { // run the animation + clipRect.animate({ + width: chart.plotSizeX + }, animation); + + // delete this function to allow it only once + this.animate = null; + } + }, + + + /** + * Draw the markers + */ + drawPoints: function(){ + var series = this, + pointAttr, + data = series.data, + chart = series.chart, + plotX, + plotY, + i, + point, + radius, + graphic; + + if (series.options.marker.enabled) { + i = data.length; + while (i--) { + point = data[i]; + plotX = point.plotX; + plotY = point.plotY; + graphic = point.graphic; + + // only draw the point if y is defined + if (plotY !== UNDEFINED && !isNaN(plotY)) { + + /* && removed this code because points stayed after zoom + point.plotX >= 0 && point.plotX <= chart.plotSizeX && + point.plotY >= 0 && point.plotY <= chart.plotSizeY*/ + + // shortcuts + pointAttr = point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]; + radius = pointAttr.r; + + if (graphic) { // update + graphic.animate({ + x: plotX, + y: plotY, + r: radius + }); + } else { + point.graphic = chart.renderer.symbol( + pick(point.marker && point.marker.symbol, series.symbol), + plotX, + plotY, + radius + ) + .attr(pointAttr) + .add(series.group); + } + } + } + } + + }, + + /** + * Convert state properties from API naming conventions to SVG attributes + * + * @param {Object} options API options object + * @param {Object} base1 SVG attribute object to inherit from + * @param {Object} base2 Second level SVG attribute object to inherit from + */ + convertAttribs: function(options, base1, base2, base3) { + var conversion = this.pointAttrToOptions, + attr, + option, + obj = {}; + + options = options || {}; + base1 = base1 || {}; + base2 = base2 || {}; + base3 = base3 || {}; + + for (attr in conversion) { + option = conversion[attr]; + obj[attr] = pick(options[option], base1[attr], base2[attr], base3[attr]); + } + return obj; + }, + + /** + * Get the state attributes. Each series type has its own set of attributes + * that are allowed to change on a point's state change. Series wide attributes are stored for + * all series, and additionally point specific attributes are stored for all + * points with individual marker options. If such options are not defined for the point, + * a reference to the series wide attributes is stored in point.pointAttr. + */ + getAttribs: function() { + var series = this, + normalOptions = defaultPlotOptions[series.type].marker ? series.options.marker : series.options, + stateOptions = normalOptions.states, + stateOptionsHover = stateOptions[HOVER_STATE], + pointStateOptionsHover, + seriesColor = series.color, + normalDefaults = { + stroke: seriesColor, + fill: seriesColor + }, + data = series.data, + i, + point, + seriesPointAttr = [], + pointAttr, + pointAttrToOptions = series.pointAttrToOptions, + hasPointSpecificOptions; + + // series type specific modifications + if (series.options.marker) { // line, spline, area, areaspline, scatter + + // if no hover radius is given, default to normal radius + 2 + stateOptionsHover.radius = stateOptionsHover.radius || normalOptions.radius + 2; + stateOptionsHover.lineWidth = stateOptionsHover.lineWidth || normalOptions.lineWidth + 1; + + } else { // column, bar, pie + + // if no hover color is given, brighten the normal color + stateOptionsHover.color = stateOptionsHover.color || + Color(stateOptionsHover.color || seriesColor) + .brighten(stateOptionsHover.brightness).get(); + } + + // general point attributes for the series normal state + seriesPointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, normalDefaults); + + // HOVER_STATE and SELECT_STATE states inherit from normal state except the default radius + each([HOVER_STATE, SELECT_STATE], function(state) { + seriesPointAttr[state] = + series.convertAttribs(stateOptions[state], seriesPointAttr[NORMAL_STATE]); + }); + + // set it + series.pointAttr = seriesPointAttr; + + + // Generate the point-specific attribute collections if specific point + // options are given. If not, create a referance to the series wide point + // attributes + i = data.length; + while (i--) { + point = data[i]; + normalOptions = (point.options && point.options.marker) || point.options; + if (normalOptions && normalOptions.enabled === false) { + normalOptions.radius = 0; + } + hasPointSpecificOptions = false; + + // check if the point has specific visual options + if (point.options) { + for (var key in pointAttrToOptions) { + if (defined(normalOptions[pointAttrToOptions[key]])) { + hasPointSpecificOptions = true; + } + } + } + + + + // a specific marker config object is defined for the individual point: + // create it's own attribute collection + if (hasPointSpecificOptions) { + + pointAttr = []; + stateOptions = normalOptions.states || {}; // reassign for individual point + pointStateOptionsHover = stateOptions[HOVER_STATE] = stateOptions[HOVER_STATE] || {}; + + // if no hover color is given, brighten the normal color + if (!series.options.marker) { // column, bar, point + pointStateOptionsHover.color = + Color(pointStateOptionsHover.color || point.options.color) + .brighten(pointStateOptionsHover.brightness || + stateOptionsHover.brightness).get(); + + } + + // normal point state inherits series wide normal state + pointAttr[NORMAL_STATE] = series.convertAttribs(normalOptions, seriesPointAttr[NORMAL_STATE]); + + // inherit from point normal and series hover + pointAttr[HOVER_STATE] = series.convertAttribs( + stateOptions[HOVER_STATE], + seriesPointAttr[HOVER_STATE], + pointAttr[NORMAL_STATE] + ); + // inherit from point normal and series hover + pointAttr[SELECT_STATE] = series.convertAttribs( + stateOptions[SELECT_STATE], + seriesPointAttr[SELECT_STATE], + pointAttr[NORMAL_STATE] + ); + + + + // no marker config object is created: copy a reference to the series-wide + // attribute collection + } else { + pointAttr = seriesPointAttr; + } + + point.pointAttr = pointAttr; + + } + + }, + + + /** + * Clear DOM objects and free up memory + */ + destroy: function() { + var series = this, + chart = series.chart, + //chartSeries = series.chart.series, + clipRect = series.clipRect, + issue134 = //5[0-9.]+ (Safari|Mobile)//.test(userAgent), // todo: update when Safari bug is fixed + destroy, + prop; + + // remove all events + removeEvent(series); + + // remove legend items + if (series.legendItem) { + series.chart.legend.destroyItem(series); + } + + // destroy all points with their elements + each(series.data, function(point) { + point.destroy(); + }); + // destroy all SVGElements associated to the series + each(['area', 'graph', 'dataLabelsGroup', 'group', 'tracker'], function(prop) { + if (series[prop]) { + + // issue 134 workaround + destroy = issue134 && prop == 'group' ? + 'hide' : + 'destroy'; + + series[prop][destroy](); + } + }); + + // remove from hoverSeries + if (chart.hoverSeries == series) { + chart.hoverSeries = null; + } + erase(chart.series, series); + + // clear all members + for (prop in series) { + delete series[prop]; + } + }, + + /** + * Draw the data labels + */ + drawDataLabels: function() { + if (this.options.dataLabels.enabled) { + var series = this, + x, + y, + data = series.data, + options = series.options.dataLabels, + str, + dataLabelsGroup = series.dataLabelsGroup, + chart = series.chart, + inverted = chart.inverted, + seriesType = series.type, + color; + + // create a separate group for the data labels to avoid rotation + if (!dataLabelsGroup) { + dataLabelsGroup = series.dataLabelsGroup = + chart.renderer.g(PREFIX +'data-labels') + .attr({ + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 5 + }) + .translate(chart.plotLeft, chart.plotTop) + .add(); + } + + // determine the color + color = options.color; + if (color == 'auto') { // 1.0 backwards compatibility + color = null; + } + options.style.color = pick(color, series.color); + + // make the labels for each point + each(data, function(point, i){ + var barX = point.barX, + plotX = barX && barX + point.barW / 2 || point.plotX || -999, + plotY = pick(point.plotY, -999), + dataLabel = point.dataLabel, + align = options.align; + + // get the string + str = point.getDataLabelText(); + x = (inverted ? chart.plotWidth - plotY : plotX) + options.x; + y = (inverted ? chart.plotHeight - plotX : plotY) + options.y; + + // in columns, align the string to the column + if (seriesType == 'column') { + x += { left: -1, right: 1 }[align] * point.barW / 2 || 0; + } + + + if (dataLabel) { + dataLabel.animate({ + x: x, + y: y + }); + } else if (defined(str)) { + dataLabel = point.dataLabel = chart.renderer.text( + str, + x, + y + ) + .attr({ + align: align, + rotation: options.rotation, + zIndex: 1 + }) + .css(options.style) + .add(dataLabelsGroup); + } + + // vertically centered + if (inverted && !options.y) { + dataLabel.attr({ + y: y + parseInt(dataLabel.styles.lineHeight) * 0.9 - dataLabel.getBBox().height / 2 + }); + } + + /*if (series.isCartesian) { + dataLabel[chart.isInsidePlot(plotX, plotY) ? 'show' : 'hide'](); + }*/ + + }); + } + }, + + /** + * Draw the actual graph + */ + drawGraph: function(state) { + var series = this, + options = series.options, + chart = series.chart, + graph = series.graph, + graphPath = [], + fillColor, + area = series.area, + group = series.group, + color = options.lineColor || series.color, + lineWidth = options.lineWidth, + dashStyle = options.dashStyle, + segmentPath, + renderer = chart.renderer, + translatedThreshold = series.yAxis.getThreshold(options.threshold || 0), + useArea = /^area/.test(series.type), + singlePoints = [], // used in drawTracker + areaPath = [], + attribs; + + + // divide into segments and build graph and area paths + each(series.segments, function(segment) { + segmentPath = []; + + // build the segment line + each(segment, function(point, i) { + + if (series.getPointSpline) { // generate the spline as defined in the SplineSeries object + segmentPath.push.apply(segmentPath, series.getPointSpline(segment, point, i)); + + } else { + + // moveTo or lineTo + segmentPath.push(i ? L : M); + + // step line? + if (i && options.step) { + var lastPoint = segment[i - 1]; + segmentPath.push( + point.plotX, + lastPoint.plotY + ); + } + + // normal line to next point + segmentPath.push( + point.plotX, + point.plotY + ); + } + }); + + // add the segment to the graph, or a single point for tracking + if (segment.length > 1) { + graphPath = graphPath.concat(segmentPath); + } else { + singlePoints.push(segment[0]); + } + + // build the area + if (useArea) { + var areaSegmentPath = [], + i, + segLength = segmentPath.length; + for (i = 0; i < segLength; i++) { + areaSegmentPath.push(segmentPath[i]); + } + if (segLength == 3) { // for animation from 1 to two points + areaSegmentPath.push(L, segmentPath[1], segmentPath[2]); + } + if (options.stacking && series.type != 'areaspline') { + // follow stack back. Todo: implement areaspline + for (i = segment.length - 1; i >= 0; i--) { + areaSegmentPath.push(segment[i].plotX, segment[i].yBottom); + } + + } else { // follow zero line back + areaSegmentPath.push( + L, + segment[segment.length - 1].plotX, + translatedThreshold, + L, + segment[0].plotX, + translatedThreshold + ); + } + areaPath = areaPath.concat(areaSegmentPath); + } + }); + + // used in drawTracker: + series.graphPath = graphPath; + series.singlePoints = singlePoints; + + // draw the area if area series or areaspline + if (useArea) { + fillColor = pick( + options.fillColor, + Color(series.color).setOpacity(options.fillOpacity || 0.75).get() + ); + if (area) { + area.animate({ d: areaPath }); + + } else { + // draw the area + series.area = series.chart.renderer.path(areaPath) + .attr({ + fill: fillColor + }).add(group); + } + } + + // draw the graph + if (graph) { + //graph.animate({ d: graphPath.join(' ') }); + graph.animate({ d: graphPath }); + + } else { + if (lineWidth) { + attribs = { + 'stroke': color, + 'stroke-width': lineWidth + }; + if (dashStyle) { + attribs.dashstyle = dashStyle; + } + + series.graph = renderer.path(graphPath) + .attr(attribs).add(group).shadow(options.shadow); + } + } + }, + + + /** + * Render the graph and markers + */ + render: function() { + var series = this, + chart = series.chart, + group, + setInvert, + options = series.options, + animation = options.animation, + doAnimation = animation && series.animate, + duration = doAnimation ? animation && animation.duration || 500 : 0, + clipRect = series.clipRect, + renderer = chart.renderer; + + + // Add plot area clipping rectangle. If this is before chart.hasRendered, + // create one shared clipRect. + if (!clipRect) { + clipRect = series.clipRect = !chart.hasRendered && chart.clipRect ? + chart.clipRect : + renderer.clipRect(0, 0, chart.plotSizeX, chart.plotSizeY); + if (!chart.clipRect) { + chart.clipRect = clipRect; + } + } + + + // the group + if (!series.group) { + group = series.group = renderer.g('series'); + + if (chart.inverted) { + setInvert = function() { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }).invert(); + }; + + setInvert(); // do it now + addEvent(chart, 'resize', setInvert); // do it on resize + } + group.clip(series.clipRect) + .attr({ + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: options.zIndex + }) + .translate(chart.plotLeft, chart.plotTop) + .add(chart.seriesGroup); + } + + series.drawDataLabels(); + + // initiate the animation + if (doAnimation) { + series.animate(true); + } + + // cache attributes for shapes + series.getAttribs(); + + // draw the graph if any + if (series.drawGraph) { + series.drawGraph(); + } + + // draw the points + series.drawPoints(); + + // draw the mouse tracking area + if (series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + // run the animation + if (doAnimation) { + series.animate(); + } + + // finish the individual clipRect + setTimeout(function() { + clipRect.isAnimating = false; + group = series.group; // can be destroyed during the timeout + if (group && clipRect != chart.clipRect && clipRect.renderer) { + group.clip((series.clipRect = chart.clipRect)); + clipRect.destroy(); + } + }, duration); + + + series.isDirty = false; // means data is in accordance with what you see + + }, + + /** + * Redraw the series after an update in the axes. + */ + redraw: function() { + var series = this, + chart = series.chart, + clipRect = series.clipRect, + group = series.group; + + /*if (clipRect) { + stop(clipRect); + clipRect.animate({ // for chart resize + width: chart.plotSizeX, + height: chart.plotSizeY + }); + }*/ + + // reposition on resize + if (group) { + if (chart.inverted) { + group.attr({ + width: chart.plotWidth, + height: chart.plotHeight + }); + } + + group.animate({ + translateX: chart.plotLeft, + translateY: chart.plotTop + }); + } + + series.translate(); + series.setTooltipPoints(true); + series.render(); + }, + + /** + * Set the state of the graph + */ + setState: function(state) { + var series = this, + options = series.options, + graph = series.graph, + stateOptions = options.states, + lineWidth = options.lineWidth; + + state = state || NORMAL_STATE; + + if (series.state != state) { + series.state = state; + + if (stateOptions[state] && stateOptions[state].enabled === false) { + return; + } + + if (state) { + lineWidth = stateOptions[state].lineWidth || lineWidth + 1; + } + + if (graph && !graph.dashstyle) { // hover is turned off for dashed lines in VML + graph.attr({ // use attr because animate will cause any other animation on the graph to stop + 'stroke-width': lineWidth + }, state ? 0 : 500); + } + } + }, + + /** + * Set the visibility of the graph + * + * @param vis {Boolean} True to show the series, false to hide. If UNDEFINED, + * the visibility is toggled. + */ + setVisible: function(vis, redraw) { + var series = this, + chart = series.chart, + legendItem = series.legendItem, + seriesGroup = series.group, + seriesTracker = series.tracker, + dataLabelsGroup = series.dataLabelsGroup, + showOrHide, + i, + data = series.data, + point, + ignoreHiddenSeries = chart.options.chart.ignoreHiddenSeries, + oldVisibility = series.visible; + + // if called without an argument, toggle visibility + series.visible = vis = vis === UNDEFINED ? !oldVisibility : vis; + showOrHide = vis ? 'show' : 'hide'; + + // show or hide series + if (seriesGroup) { // pies don't have one + seriesGroup[showOrHide](); + } + + // show or hide trackers + if (seriesTracker) { + seriesTracker[showOrHide](); + } else { + i = data.length; + while (i--) { + point = data[i]; + if (point.tracker) { + point.tracker[showOrHide](); + } + } + } + + + if (dataLabelsGroup) { + dataLabelsGroup[showOrHide](); + } + + if (legendItem) { + chart.legend.colorizeItem(series, vis); + } + + + // rescale or adapt to resized chart + series.isDirty = true; + // in a stack, all other series are affected + if (series.options.stacking) { + each(chart.series, function(otherSeries) { + if (otherSeries.options.stacking && otherSeries.visible) { + otherSeries.isDirty = true; + } + }); + } + + if (ignoreHiddenSeries) { + chart.isDirtyBox = true; + } + if (redraw !== false) { + chart.redraw(); + } + + fireEvent(series, showOrHide); + }, + + /** + * Show the graph + */ + show: function() { + this.setVisible(true); + }, + + /** + * Hide the graph + */ + hide: function() { + this.setVisible(false); + }, + + + /** + * Set the selected state of the graph + * + * @param selected {Boolean} True to select the series, false to unselect. If + * UNDEFINED, the selection state is toggled. + */ + select: function(selected) { + var series = this; + // if called without an argument, toggle + series.selected = selected = (selected === UNDEFINED) ? !series.selected : selected; + + if (series.checkbox) { + series.checkbox.checked = selected; + } + + fireEvent(series, selected ? 'select' : 'unselect'); + }, + + + /** + * Draw the tracker object that sits above all data labels and markers to + * track mouse events on the graph or points. For the line type charts + * the tracker uses the same graphPath, but with a greater stroke width + * for better control. + */ + drawTracker: function() { + var series = this, + options = series.options, + trackerPath = [].concat(series.graphPath), + trackerPathLength = trackerPath.length, + chart = series.chart, + snap = chart.options.tooltip.snap, + tracker = series.tracker, + cursor = options.cursor, + css = cursor && { cursor: cursor }, + singlePoints = series.singlePoints, + singlePoint, + i; + + // Extend end points. A better way would be to use round linecaps, + // but those are not clickable in VML. + if (trackerPathLength) { + i = trackerPathLength + 1; + while (i--) { + if (trackerPath[i] == M) { // extend left side + trackerPath.splice(i + 1, 0, trackerPath[i + 1] - snap, trackerPath[i + 2], L); + } + if ((i && trackerPath[i] == M) || i == trackerPathLength) { // extend right side + trackerPath.splice(i, 0, L, trackerPath[i - 2] + snap, trackerPath[i - 1]); + } + } + } + + // handle single points + for (i = 0; i < singlePoints.length; i++) { + singlePoint = singlePoints[i]; + trackerPath.push(M, singlePoint.plotX - snap, singlePoint.plotY, + L, singlePoint.plotX + snap, singlePoint.plotY); + } + + // draw the tracker + if (tracker) { + tracker.attr({ d: trackerPath }); + + } else { // create + series.tracker = chart.renderer.path(trackerPath) + .attr({ + isTracker: true, + stroke: TRACKER_FILL, + fill: NONE, + 'stroke-width' : options.lineWidth + 2 * snap, + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 1 + }) + .on(hasTouch ? 'touchstart' : 'mouseover', function() { + if (chart.hoverSeries != series) { + series.onMouseOver(); + } + }) + .on('mouseout', function() { + if (!options.stickyTracking) { + series.onMouseOut(); + } + }) + .css(css) + .add(chart.trackerGroup); + } + + } + +}; // end Series prototype + + +/** + * LineSeries object + */ +var LineSeries = extendClass(Series); +seriesTypes.line = LineSeries; + +/** + * AreaSeries object + */ +var AreaSeries = extendClass(Series, { + type: 'area' +}); +seriesTypes.area = AreaSeries; + + + + +/** + * SplineSeries object + */ +var SplineSeries = extendClass( Series, { + type: 'spline', + + /** + * Draw the actual graph + */ + getPointSpline: function(segment, point, i) { + var smoothing = 1.5, // 1 means control points midway between points, 2 means 1/3 from the point, 3 is 1/4 etc + denom = smoothing + 1, + plotX = point.plotX, + plotY = point.plotY, + lastPoint = segment[i - 1], + nextPoint = segment[i + 1], + leftContX, + leftContY, + rightContX, + rightContY, + ret; + + // find control points + if (i && i < segment.length - 1) { + var lastX = lastPoint.plotX, + lastY = lastPoint.plotY, + nextX = nextPoint.plotX, + nextY = nextPoint.plotY, + correction; + + leftContX = (smoothing * plotX + lastX) / denom; + leftContY = (smoothing * plotY + lastY) / denom; + rightContX = (smoothing * plotX + nextX) / denom; + rightContY = (smoothing * plotY + nextY) / denom; + + // have the two control points make a straight line through main point + correction = ((rightContY - leftContY) * (rightContX - plotX)) / + (rightContX - leftContX) + plotY - rightContY; + + leftContY += correction; + rightContY += correction; + + // to prevent false extremes, check that control points are between + // neighbouring points' y values + if (leftContY > lastY && leftContY > plotY) { + leftContY = mathMax(lastY, plotY); + rightContY = 2 * plotY - leftContY; // mirror of left control point + } else if (leftContY < lastY && leftContY < plotY) { + leftContY = mathMin(lastY, plotY); + rightContY = 2 * plotY - leftContY; + } + if (rightContY > nextY && rightContY > plotY) { + rightContY = mathMax(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } else if (rightContY < nextY && rightContY < plotY) { + rightContY = mathMin(nextY, plotY); + leftContY = 2 * plotY - rightContY; + } + + // record for drawing in next point + point.rightContX = rightContX; + point.rightContY = rightContY; + + } + + // moveTo or lineTo + if (!i) { + ret = [M, plotX, plotY]; + } + + // curve from last point to this + else { + ret = [ + 'C', + lastPoint.rightContX || lastPoint.plotX, + lastPoint.rightContY || lastPoint.plotY, + leftContX || plotX, + leftContY || plotY, + plotX, + plotY + ]; + lastPoint.rightContX = lastPoint.rightContY = null; // reset for updating series later + } + return ret; + } +}); +seriesTypes.spline = SplineSeries; + + + +/** + * AreaSplineSeries object + */ +var AreaSplineSeries = extendClass(SplineSeries, { + type: 'areaspline' +}); +seriesTypes.areaspline = AreaSplineSeries; + +/** + * ColumnSeries object + */ +var ColumnSeries = extendClass(Series, { + type: 'column', + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color', + r: 'borderRadius' + }, + init: function() { + Series.prototype.init.apply(this, arguments); + + var series = this, + chart = series.chart; + + // flag the chart in order to pad the x axis + chart.hasColumn = true; + + // if the series is added dynamically, force redraw of other + // series affected by a new column + if (chart.hasRendered) { + each(chart.series, function(otherSeries) { + if (otherSeries.type == series.type) { + otherSeries.isDirty = true; + } + }); + } + }, + + /** + * Translate each point to the plot area coordinate system and find shape positions + */ + translate: function() { + var series = this, + chart = series.chart, + columnCount = 0, + reversedXAxis = series.xAxis.reversed, + categories = series.xAxis.categories, + stackGroups = {}, + stackKey, + columnIndex; + + Series.prototype.translate.apply(series); + + // Get the total number of column type series. + // This is called on every series. Consider moving this logic to a + // chart.orderStacks() function and call it on init, addSeries and removeSeries + each(chart.series, function(otherSeries) { + if (otherSeries.type == series.type) { + if (otherSeries.options.stacking) { + stackKey = otherSeries.stackKey; + if (stackGroups[stackKey] === UNDEFINED) { + stackGroups[stackKey] = columnCount++; + } + columnIndex = stackGroups[stackKey]; + } else if (otherSeries.visible){ + columnIndex = columnCount++; + } + otherSeries.columnIndex = columnIndex; + } + }); + + // calculate the width and position of each column based on + // the number of column series in the plot, the groupPadding + // and the pointPadding options + var options = series.options, + data = series.data, + closestPoints = series.closestPoints, + categoryWidth = mathAbs( + data[1] ? data[closestPoints].plotX - data[closestPoints - 1].plotX : + chart.plotSizeX / (categories ? categories.length : 1) + ), + groupPadding = categoryWidth * options.groupPadding, + groupWidth = categoryWidth - 2 * groupPadding, + pointOffsetWidth = groupWidth / columnCount, + optionPointWidth = options.pointWidth, + pointPadding = defined(optionPointWidth) ? (pointOffsetWidth - optionPointWidth) / 2 : + pointOffsetWidth * options.pointPadding, + pointWidth = mathMax(pick(optionPointWidth, pointOffsetWidth - 2 * pointPadding), 1), + colIndex = (reversedXAxis ? columnCount - + series.columnIndex : series.columnIndex) || 0, + pointXOffset = pointPadding + (groupPadding + colIndex * + pointOffsetWidth -(categoryWidth / 2)) * + (reversedXAxis ? -1 : 1), + threshold = options.threshold || 0, + translatedThreshold = series.yAxis.getThreshold(threshold), + minPointLength = pick(options.minPointLength, 5); + + // record the new values + each(data, function(point) { + var plotY = point.plotY, + yBottom = point.yBottom || translatedThreshold, + barX = point.plotX + pointXOffset, + barY = mathCeil(mathMin(plotY, yBottom)), + barH = mathCeil(mathMax(plotY, yBottom) - barY), + trackerY; + + // handle options.minPointLength and tracker for small points + if (mathAbs(barH) < minPointLength) { + if (minPointLength) { + barH = minPointLength; + barY = + mathAbs(barY - translatedThreshold) > minPointLength ? // stacked + yBottom - minPointLength : // keep position + translatedThreshold - (plotY <= translatedThreshold ? minPointLength : 0); + } + trackerY = barY - 3; + } + + extend(point, { + barX: barX, + barY: barY, + barW: pointWidth, + barH: barH + }); + point.shapeType = 'rect'; + point.shapeArgs = { + x: barX, + y: barY, + width: pointWidth, + height: barH, + r: options.borderRadius + }; + + // make small columns responsive to mouse + point.trackerArgs = defined(trackerY) && merge(point.shapeArgs, { + height: mathMax(6, barH + 3), + y: trackerY + }); + }); + + }, + + getSymbol: function(){ + }, + + /** + * Columns have no graph + */ + drawGraph: function() {}, + + /** + * Draw the columns. For bars, the series.group is rotated, so the same coordinates + * apply for columns and bars. This method is inherited by scatter series. + * + */ + drawPoints: function() { + var series = this, + options = series.options, + renderer = series.chart.renderer, + graphic, + shapeArgs; + + + // draw the columns + each(series.data, function(point) { + var plotY = point.plotY; + if (plotY !== UNDEFINED && !isNaN(plotY)) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + if (graphic) { // update + stop(graphic); + graphic.animate(shapeArgs); + + } else { + point.graphic = renderer[point.shapeType](shapeArgs) + .attr(point.pointAttr[point.selected ? SELECT_STATE : NORMAL_STATE]) + .add(series.group) + .shadow(options.shadow); + } + + } + }); + }, + /** + * Draw the individual tracker elements. + * This method is inherited by scatter and pie charts too. + */ + drawTracker: function() { + var series = this, + chart = series.chart, + renderer = chart.renderer, + shapeArgs, + tracker, + trackerLabel = +new Date(), + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + rel; + + each(series.data, function(point) { + tracker = point.tracker; + shapeArgs = point.trackerArgs || point.shapeArgs; + if (point.y !== null) { + if (tracker) {// update + tracker.attr(shapeArgs); + + } else { + point.tracker = + renderer[point.shapeType](shapeArgs) + .attr({ + isTracker: trackerLabel, + fill: TRACKER_FILL, + visibility: series.visible ? VISIBLE : HIDDEN, + zIndex: 1 + }) + .on(hasTouch ? 'touchstart' : 'mouseover', function(event) { + rel = event.relatedTarget || event.fromElement; + if (chart.hoverSeries != series && attr(rel, 'isTracker') != trackerLabel) { + series.onMouseOver(); + } + point.onMouseOver(); + + }) + .on('mouseout', function(event) { + if (!series.options.stickyTracking) { + rel = event.relatedTarget || event.toElement; + if (attr(rel, 'isTracker') != trackerLabel) { + series.onMouseOut(); + } + } + }) + .css(css) + .add(chart.trackerGroup); + } + } + }); + }, + + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function(init) { + var series = this, + data = series.data; + + if (!init) { // run the animation + /* + * Note: Ideally the animation should be initialized by calling + * series.group.hide(), and then calling series.group.show() + * after the animation was started. But this rendered the shadows + * invisible in IE8 standards mode. If the columns flicker on large + * datasets, this is the cause. + */ + + each(data, function(point) { + var graphic = point.graphic; + + if (graphic) { + // start values + graphic.attr({ + height: 0, + y: series.yAxis.translate(0, 0, 1) + }); + + // animate + graphic.animate({ + height: point.barH, + y: point.barY + }, series.options.animation); + } + }); + + + // delete this function to allow it only once + series.animate = null; + } + + }, + /** + * Remove this series from the chart + */ + remove: function() { + var series = this, + chart = series.chart; + + // column and bar series affects other series of the same type + // as they are either stacked or grouped + if (chart.hasRendered) { + each(chart.series, function(otherSeries) { + if (otherSeries.type == series.type) { + otherSeries.isDirty = true; + } + }); + } + + Series.prototype.remove.apply(series, arguments); + } +}); +seriesTypes.column = ColumnSeries; + +var BarSeries = extendClass(ColumnSeries, { + type: 'bar', + init: function(chart) { + chart.inverted = this.inverted = true; + ColumnSeries.prototype.init.apply(this, arguments); + } +}); +seriesTypes.bar = BarSeries; + +/** + * The scatter series class + */ +var ScatterSeries = extendClass(Series, { + type: 'scatter', + + /** + * Extend the base Series' translate method by adding shape type and + * arguments for the point trackers + */ + translate: function() { + var series = this; + + Series.prototype.translate.apply(series); + + each(series.data, function(point) { + point.shapeType = 'circle'; + point.shapeArgs = { + x: point.plotX, + y: point.plotY, + r: series.chart.options.tooltip.snap + }; + }); + }, + + + /** + * Create individual tracker elements for each point + */ + //drawTracker: ColumnSeries.prototype.drawTracker, + drawTracker: function() { + var series = this, + cursor = series.options.cursor, + css = cursor && { cursor: cursor }, + graphic; + + each(series.data, function(point) { + graphic = point.graphic; + if (graphic) { // doesn't exist for null points + graphic + .attr({ isTracker: true }) + .on('mouseover', function(event) { + series.onMouseOver(); + point.onMouseOver(); + }) + .on('mouseout', function(event) { + if (!series.options.stickyTracking) { + series.onMouseOut(); + } + }) + .css(css); + } + }); + + }, + + /** + * Cleaning the data is not necessary in a scatter plot + */ + cleanData: function() {} +}); +seriesTypes.scatter = ScatterSeries; + +/** + * Extended point object for pies + */ +var PiePoint = extendClass(Point, { + /** + * Initiate the pie slice + */ + init: function () { + + Point.prototype.init.apply(this, arguments); + + var point = this, + toggleSlice; + + //visible: options.visible !== false, + extend(point, { + visible: point.visible !== false, + name: pick(point.name, 'Slice') + }); + + // add event listener for select + toggleSlice = function() { + point.slice(); + }; + addEvent(point, 'select', toggleSlice); + addEvent(point, 'unselect', toggleSlice); + + return point; + }, + + /** + * Toggle the visibility of the pie slice + * @param {Boolean} vis Whether to show the slice or not. If undefined, the + * visibility is toggled + */ + setVisible: function(vis) { + var point = this, + chart = point.series.chart, + tracker = point.tracker, + dataLabel = point.dataLabel, + connector = point.connector, + method; + + // if called without an argument, toggle visibility + point.visible = vis = vis === UNDEFINED ? !point.visible : vis; + + method = vis ? 'show' : 'hide'; + + point.group[method](); + if (tracker) { + tracker[method](); + } + if (dataLabel) { + dataLabel[method](); + } + if (connector) { + connector[method](); + } + if (point.legendItem) { + chart.legend.colorizeItem(point, vis); + } + }, + + /** + * Set or toggle whether the slice is cut out from the pie + * @param {Boolean} sliced When undefined, the slice state is toggled + * @param {Boolean} redraw Whether to redraw the chart. True by default. + */ + slice: function(sliced, redraw, animation) { + var point = this, + series = point.series, + chart = series.chart, + slicedTranslation = point.slicedTranslation; + + setAnimation(animation, chart); + + // redraw is true by default + redraw = pick(redraw, true); + + // if called without an argument, toggle + sliced = point.sliced = defined(sliced) ? sliced : !point.sliced; + + point.group.animate({ + translateX: (sliced ? slicedTranslation[0] : chart.plotLeft), + translateY: (sliced ? slicedTranslation[1] : chart.plotTop) + }); + + } +}); + +/** + * The Pie series class + */ +var PieSeries = extendClass(Series, { + type: 'pie', + isCartesian: false, + pointClass: PiePoint, + pointAttrToOptions: { // mapping between SVG attributes and the corresponding options + stroke: 'borderColor', + 'stroke-width': 'borderWidth', + fill: 'color' + }, + + /** + * Pies have one color each point + */ + getColor: function() { + // record first color for use in setData + this.initialColor = colorCounter; + }, + + /** + * Animate the column heights one by one from zero + * @param {Boolean} init Whether to initialize the animation or run it + */ + animate: function(init) { + var series = this, + data = series.data; + + each(data, function(point) { + var graphic = point.graphic, + args = point.shapeArgs, + up = -mathPI / 2; + + if (graphic) { + // start values + graphic.attr({ + r: 0, + start: up, + end: up + }); + + // animate + graphic.animate({ + r: args.r, + start: args.start, + end: args.end + }, series.options.animation); + } + }); + + // delete this function to allow it only once + series.animate = null; + + }, + /** + * Do translation for pie slices + */ + translate: function() { + var total = 0, + series = this, + cumulative = -0.25, // start at top + precision = 1000, // issue #172 + options = series.options, + slicedOffset = options.slicedOffset, + connectorOffset = slicedOffset + options.borderWidth, + positions = options.center, + chart = series.chart, + plotWidth = chart.plotWidth, + plotHeight = chart.plotHeight, + start, + end, + angle, + data = series.data, + circ = 2 * mathPI, + fraction, + smallestSize = mathMin(plotWidth, plotHeight), + isPercent, + radiusX, // the x component of the radius vector for a given point + radiusY, + labelDistance = options.dataLabels.distance; + + // get positions - either an integer or a percentage string must be given + positions.push(options.size, options.innerSize || 0); + positions = map(positions, function(length, i) { + + isPercent = /%$/.test(length); + return isPercent ? + // i == 0: centerX, relative to width + // i == 1: centerY, relative to height + // i == 2: size, relative to smallestSize + [plotWidth, plotHeight, smallestSize, smallestSize][i] * + pInt(length) / 100: + length; + }); + + // utility for getting the x value from a given y, used for anticollision logic in data labels + series.getX = function(y, left) { + + angle = math.asin((y - positions[1]) / (positions[2] / 2 + labelDistance)); + + return positions[0] + + (left ? -1 : 1) * + (mathCos(angle) * (positions[2] / 2 + labelDistance)); + }; + + // set center for later use + series.center = positions; + + // get the total sum + each(data, function(point) { + total += point.y; + }); + + each(data, function(point) { + // set start and end angle + fraction = total ? point.y / total : 0; + start = mathRound(cumulative * circ * precision) / precision; + cumulative += fraction; + end = mathRound(cumulative * circ * precision) / precision; + + // set the shape + point.shapeType = 'arc'; + point.shapeArgs = { + x: positions[0], + y: positions[1], + r: positions[2] / 2, + innerR: positions[3] / 2, + start: start, + end: end + }; + + // center for the sliced out slice + angle = (end + start) / 2; + point.slicedTranslation = map([ + mathCos(angle) * slicedOffset + chart.plotLeft, + mathSin(angle) * slicedOffset + chart.plotTop + ], mathRound); + + // set the anchor point for tooltips + radiusX = mathCos(angle) * positions[2] / 2; + radiusY = mathSin(angle) * positions[2] / 2; + point.tooltipPos = [ + positions[0] + radiusX * 0.7, + positions[1] + radiusY * 0.7 + ]; + + // set the anchor point for data labels + point.labelPos = [ + positions[0] + radiusX + mathCos(angle) * labelDistance, // first break of connector + positions[1] + radiusY + mathSin(angle) * labelDistance, // a/a + positions[0] + radiusX + mathCos(angle) * connectorOffset, // second break, right outside pie + positions[1] + radiusY + mathSin(angle) * connectorOffset, // a/a + positions[0] + radiusX, // landing point for connector + positions[1] + radiusY, // a/a + labelDistance < 0 ? // alignment + 'center' : + angle < circ / 4 ? 'left' : 'right', // alignment + angle // center angle + ]; + + + // API properties + point.percentage = fraction * 100; + point.total = total; + + }); + + this.setTooltipPoints(); + }, + + /** + * Render the slices + */ + render: function() { + var series = this; + + // cache attributes for shapes + series.getAttribs(); + + this.drawPoints(); + + // draw the mouse tracking area + if (series.options.enableMouseTracking !== false) { + series.drawTracker(); + } + + this.drawDataLabels(); + + if (series.options.animation && series.animate) { + series.animate(); + } + + series.isDirty = false; // means data is in accordance with what you see + }, + + /** + * Draw the data points + */ + drawPoints: function() { + var series = this, + chart = series.chart, + renderer = chart.renderer, + groupTranslation, + //center, + graphic, + group, + shapeArgs; + + // draw the slices + each(series.data, function(point) { + graphic = point.graphic; + shapeArgs = point.shapeArgs; + group = point.group; + + // create the group the first time + if (!group) { + group = point.group = renderer.g('point') + .attr({ zIndex: 5 }) + .add(); + } + + // if the point is sliced, use special translation, else use plot area traslation + groupTranslation = point.sliced ? point.slicedTranslation : [chart.plotLeft, chart.plotTop]; + group.translate(groupTranslation[0], groupTranslation[1]) + + + // draw the slice + if (graphic) { + graphic.animate(shapeArgs); + } else { + point.graphic = + renderer.arc(shapeArgs) + .attr(extend( + point.pointAttr[NORMAL_STATE], + { 'stroke-linejoin': 'round' } + )) + .add(point.group); + } + + // detect point specific visibility + if (point.visible === false) { + point.setVisible(false); + } + + }); + + }, + + /** + * Override the base drawDataLabels method by pie specific functionality + */ + drawDataLabels: function() { + var series = this, + data = series.data, + point, + chart = series.chart, + options = series.options.dataLabels, + connectorPadding = pick(options.connectorPadding, 10), + connectorWidth = pick(options.connectorWidth, 1), + connector, + connectorPath, + outside = options.distance > 0, + dataLabel, + labelPos, + labelHeight, + lastY, + centerY = series.center[1], + quarters = [// divide the points into quarters for anti collision + [], // top right + [], // bottom right + [], // bottom left + [] // top left + ], + x, + y, + visibility, + overlapping, + rankArr, + secondPass, + sign, + lowerHalf, + sort, + i = 4, + j; + + // run parent method + Series.prototype.drawDataLabels.apply(series); + + // arrange points for detection collision + each(data, function(point) { + var angle = point.labelPos[7], + quarter; + if (angle < 0) { + quarter = 0; + } else if (angle < mathPI / 2) { + quarter = 1; + } else if (angle < mathPI) { + quarter = 2; + } else { + quarter = 3; + } + quarters[quarter].push(point); + }); + quarters[1].reverse(); + quarters[3].reverse(); + + // define the sorting algorithm + sort = function(a,b) { + return a.y > b.y; + }; + /* Loop over the points in each quartile, starting from the top and bottom + * of the pie to detect overlapping labels. + */ + while (i--) { + overlapping = 0; + + // create an array for sorting and ranking the points within each quarter + rankArr = [].concat(quarters[i]); + rankArr.sort(sort); + j = rankArr.length; + while (j--) { + rankArr[j].rank = j; + } + + /* In the first pass, count the number of overlapping labels. In the second + * pass, remove the labels with lowest rank/values. + */ + for (secondPass = 0; secondPass < 2; secondPass++) { + lowerHalf = i % 3; + lastY = lowerHalf ? 9999 : -9999; + sign = lowerHalf ? -1 : 1; + + for (j = 0; j < quarters[i].length; j++) { + point = quarters[i][j]; + + if ((dataLabel = point.dataLabel)) { + labelPos = point.labelPos; + visibility = VISIBLE; + x = labelPos[0]; + y = labelPos[1]; + + + // assume all labels have equal height + if (!labelHeight) { + labelHeight = dataLabel && dataLabel.getBBox().height; + } + + // anticollision + if (outside) { + if (secondPass && point.rank < overlapping) { + visibility = HIDDEN; + } else if ((!lowerHalf && y < lastY + labelHeight) || + (lowerHalf && y > lastY - labelHeight)) { + y = lastY + sign * labelHeight; + x = series.getX(y, i > 1); + if ((!lowerHalf && y + labelHeight > centerY) || + (lowerHalf && y -labelHeight < centerY)) { + if (secondPass) { + visibility = HIDDEN; + } else { + overlapping++; + } + } + } + } + + if (point.visible === false) { + visibility = HIDDEN; + } + + if (visibility == VISIBLE) { + lastY = y; + } + + if (secondPass) { + + // move or place the data label + dataLabel + .attr({ + visibility: visibility, + align: labelPos[6] + }) + [dataLabel.moved ? 'animate' : 'attr']({ + x: x + options.x + + ({ left: connectorPadding, right: -connectorPadding }[labelPos[6]] || 0), + y: y + options.y + }); + dataLabel.moved = true; + + // draw the connector + if (outside && connectorWidth) { + connector = point.connector; + + connectorPath = [ + M, + x + (labelPos[6] == 'left' ? 5 : -5), y, // end of the string at the label + L, + x, y, // first break, next to the label + L, + labelPos[2], labelPos[3], // second break + L, + labelPos[4], labelPos[5] // base + ]; + + if (connector) { + connector.animate({ d: connectorPath }); + connector.attr('visibility', visibility); + + } else { + point.connector = connector = series.chart.renderer.path(connectorPath).attr({ + 'stroke-width': connectorWidth, + stroke: options.connectorColor || '#606060', + visibility: visibility, + zIndex: 3 + }) + .translate(chart.plotLeft, chart.plotTop) + .add(); + } + } + } + } + } + } + } + }, + + /** + * Draw point specific tracker objects. Inherit directly from column series. + */ + drawTracker: ColumnSeries.prototype.drawTracker, + + /** + * Pies don't have point marker symbols + */ + getSymbol: function() {} + +}); +seriesTypes.pie = PieSeries; + + +// global variables +win.Highcharts = { + Chart: Chart, + dateFormat: dateFormat, + pathAnim: pathAnim, + getOptions: getOptions, + numberFormat: numberFormat, + Point: Point, + Color: Color, + Renderer: Renderer, + seriesTypes: seriesTypes, + setOptions: setOptions, + Series: Series, + + // Expose utility funcitons for modules + addEvent: addEvent, + createElement: createElement, + discardElement: discardElement, + css: css, + each: each, + extend: extend, + map: map, + merge: merge, + pick: pick, + extendClass: extendClass, + version: '2.1.4' +}; +})(); + diff --git a/js/jquery/jquery.cookie.js b/js/jquery/jquery.cookie.js new file mode 100644 index 0000000..61d3bce --- /dev/null +++ b/js/jquery/jquery.cookie.js @@ -0,0 +1,91 @@ +/*jslint browser: true */ /*global jQuery: true */ + +/** + * jQuery Cookie plugin + * + * Copyright (c) 2010 Klaus Hartl (stilbuero.de) + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ + +// TODO JsDoc + +/** + * Create a cookie with the given key and value and other optional parameters. + * + * @example $.cookie('the_cookie', 'the_value'); + * @desc Set the value of a cookie. + * @example $.cookie('the_cookie', 'the_value', { expires: 7, path: '/', domain: 'jquery.com', secure: true }); + * @desc Create a cookie with all available options. + * @example $.cookie('the_cookie', 'the_value'); + * @desc Create a session cookie. + * @example $.cookie('the_cookie', null); + * @desc Delete a cookie by passing null as value. Keep in mind that you have to use the same path and domain + * used when the cookie was set. + * + * @param String key The key of the cookie. + * @param String value The value of the cookie. + * @param Object options An object literal containing key/value pairs to provide optional cookie attributes. + * @option Number|Date expires Either an integer specifying the expiration date from now on in days or a Date object. + * If a negative value is specified (e.g. a date in the past), the cookie will be deleted. + * If set to null or omitted, the cookie will be a session cookie and will not be retained + * when the the browser exits. + * @option String path The value of the path atribute of the cookie (default: path of page that created the cookie). + * @option String domain The value of the domain attribute of the cookie (default: domain of page that created the cookie). + * @option Boolean secure If true, the secure attribute of the cookie will be set and the cookie transmission will + * require a secure protocol (like HTTPS). + * @type undefined + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ + +/** + * Get the value of a cookie with the given key. + * + * @example $.cookie('the_cookie'); + * @desc Get the value of a cookie. + * + * @param String key The key of the cookie. + * @return The value of the cookie. + * @type String + * + * @name $.cookie + * @cat Plugins/Cookie + * @author Klaus Hartl/klaus.hartl@stilbuero.de + */ +jQuery.cookie = function (key, value, options) { + + // key and at least value given, set cookie... + if (arguments.length > 1 && String(value) !== "[object Object]") { + options = jQuery.extend({}, options); + + if (value === null || value === undefined) { + options.expires = -1; + } + + if (typeof options.expires === 'number') { + var days = options.expires, t = options.expires = new Date(); + t.setDate(t.getDate() + days); + } + + value = String(value); + + return (document.cookie = [ + encodeURIComponent(key), '=', + options.raw ? value : encodeURIComponent(value), + options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE + options.path ? '; path=' + options.path : '', + options.domain ? '; domain=' + options.domain : '', + options.secure ? '; secure' : '' + ].join('')); + } + + // key and possibly options given, get cookie... + options = value || {}; + var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent; + return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null; +}; diff --git a/js/jquery/jquery.tablesorter.js b/js/jquery/jquery.tablesorter.js new file mode 100644 index 0000000..e8e2323 --- /dev/null +++ b/js/jquery/jquery.tablesorter.js @@ -0,0 +1,1031 @@ +/* + * + * TableSorter 2.0 - Client-side table sorting with ease! + * Version 2.0.5b + * @requires jQuery v1.2.3 + * + * Copyright (c) 2007 Christian Bach + * Examples and docs at: http://tablesorter.com + * Dual licensed under the MIT and GPL licenses: + * http://www.opensource.org/licenses/mit-license.php + * http://www.gnu.org/licenses/gpl.html + * + */ +/** + * + * @description Create a sortable table with multi-column sorting capabilitys + * + * @example $('table').tablesorter(); + * @desc Create a simple tablesorter interface. + * + * @example $('table').tablesorter({ sortList:[[0,0],[1,0]] }); + * @desc Create a tablesorter interface and sort on the first and secound column column headers. + * + * @example $('table').tablesorter({ headers: { 0: { sorter: false}, 1: {sorter: false} } }); + * + * @desc Create a tablesorter interface and disableing the first and second column headers. + * + * + * @example $('table').tablesorter({ headers: { 0: {sorter:"integer"}, 1: {sorter:"currency"} } }); + * + * @desc Create a tablesorter interface and set a column parser for the first + * and second column. + * + * + * @param Object + * settings An object literal containing key/value pairs to provide + * optional settings. + * + * + * @option String cssHeader (optional) A string of the class name to be appended + * to sortable tr elements in the thead of the table. Default value: + * "header" + * + * @option String cssAsc (optional) A string of the class name to be appended to + * sortable tr elements in the thead on a ascending sort. Default value: + * "headerSortUp" + * + * @option String cssDesc (optional) A string of the class name to be appended + * to sortable tr elements in the thead on a descending sort. Default + * value: "headerSortDown" + * + * @option String sortInitialOrder (optional) A string of the inital sorting + * order can be asc or desc. Default value: "asc" + * + * @option String sortMultisortKey (optional) A string of the multi-column sort + * key. Default value: "shiftKey" + * + * @option String textExtraction (optional) A string of the text-extraction + * method to use. For complex html structures inside td cell set this + * option to "complex", on large tables the complex option can be slow. + * Default value: "simple" + * + * @option Object headers (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortList (optional) An array containing the forces sorting + * rules. This option let's you specify a default sorting rule. Default + * value: null + * + * @option Array sortForce (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * prepended to user-selected rules. Default value: null + * + * @option Boolean sortLocaleCompare (optional) Boolean flag indicating whatever + * to use String.localeCampare method or not. Default set to true. + * + * + * @option Array sortAppend (optional) An array containing forced sorting rules. + * This option let's you specify a default sorting rule, which is + * appended to user-selected rules. Default value: null + * + * @option Boolean widthFixed (optional) Boolean flag indicating if tablesorter + * should apply fixed widths to the table columns. This is usefull when + * using the pager companion plugin. This options requires the dimension + * jquery plugin. Default value: false + * + * @option Boolean cancelSelection (optional) Boolean flag indicating if + * tablesorter should cancel selection of the table headers text. + * Default value: true + * + * @option Boolean debug (optional) Boolean flag indicating if tablesorter + * should display debuging information usefull for development. + * + * @type jQuery + * + * @name tablesorter + * + * @cat Plugins/Tablesorter + * + * @author Christian Bach/christian.bach@polyester.se + */ + +(function ($) { + $.extend({ + tablesorter: new + function () { + + var parsers = [], + widgets = []; + + this.defaults = { + cssHeader: "header", + cssAsc: "headerSortUp", + cssDesc: "headerSortDown", + cssChildRow: "expand-child", + sortInitialOrder: "asc", + sortMultiSortKey: "shiftKey", + sortForce: null, + sortAppend: null, + sortLocaleCompare: true, + textExtraction: "simple", + parsers: {}, widgets: [], + widgetZebra: { + css: ["even", "odd"] + }, headers: {}, widthFixed: false, + cancelSelection: true, + sortList: [], + headerList: [], + dateFormat: "us", + decimal: '/.|,/g', + onRenderHeader: null, + selectorHeaders: 'thead th', + debug: false + }; + + /* debuging utils */ + + function benchmark(s, d) { + log(s + "," + (new Date().getTime() - d.getTime()) + "ms"); + } + + this.benchmark = benchmark; + + function log(s) { + if (typeof console != "undefined" && typeof console.debug != "undefined") { + console.log(s); + } else { + alert(s); + } + } + + /* parsers utils */ + + function buildParserCache(table, $headers) { + + if (table.config.debug) { + var parsersDebug = ""; + } + + if (table.tBodies.length == 0) return; // In the case of empty tables + var rows = table.tBodies[0].rows; + + if (rows[0]) { + + var list = [], + cells = rows[0].cells, + l = cells.length; + + for (var i = 0; i < l; i++) { + + var p = false; + + if ($.metadata && ($($headers[i]).metadata() && $($headers[i]).metadata().sorter)) { + + p = getParserById($($headers[i]).metadata().sorter); + + } else if ((table.config.headers[i] && table.config.headers[i].sorter)) { + + p = getParserById(table.config.headers[i].sorter); + } + if (!p) { + + p = detectParserForColumn(table, rows, -1, i); + } + + if (table.config.debug) { + parsersDebug += "column:" + i + " parser:" + p.id + "\n"; + } + + list.push(p); + } + } + + if (table.config.debug) { + log(parsersDebug); + } + + return list; + }; + + function detectParserForColumn(table, rows, rowIndex, cellIndex) { + var l = parsers.length, + node = false, + nodeValue = false, + keepLooking = true; + while (nodeValue == '' && keepLooking) { + rowIndex++; + if (rows[rowIndex]) { + node = getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex); + nodeValue = trimAndGetNodeText(table.config, node); + if (table.config.debug) { + log('Checking if value was empty on row:' + rowIndex); + } + } else { + keepLooking = false; + } + } + for (var i = 1; i < l; i++) { + if (parsers[i].is(nodeValue, table, node)) { + return parsers[i]; + } + } + // 0 is always the generic parser (text) + return parsers[0]; + } + + function getNodeFromRowAndCellIndex(rows, rowIndex, cellIndex) { + return rows[rowIndex].cells[cellIndex]; + } + + function trimAndGetNodeText(config, node) { + return $.trim(getElementText(config, node)); + } + + function getParserById(name) { + var l = parsers.length; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == name.toLowerCase()) { + return parsers[i]; + } + } + return false; + } + + /* utils */ + + function buildCache(table) { + + if (table.config.debug) { + var cacheTime = new Date(); + } + + var totalRows = (table.tBodies[0] && table.tBodies[0].rows.length) || 0, + totalCells = (table.tBodies[0].rows[0] && table.tBodies[0].rows[0].cells.length) || 0, + parsers = table.config.parsers, + cache = { + row: [], + normalized: [] + }; + + for (var i = 0; i < totalRows; ++i) { + + /** Add the table data to main data array */ + var c = $(table.tBodies[0].rows[i]), + cols = []; + + // if this is a child row, add it to the last row's children and + // continue to the next row + if (c.hasClass(table.config.cssChildRow)) { + cache.row[cache.row.length - 1] = cache.row[cache.row.length - 1].add(c); + // go to the next for loop + continue; + } + + cache.row.push(c); + + for (var j = 0; j < totalCells; ++j) { + cols.push(parsers[j].format(getElementText(table.config, c[0].cells[j]), table, c[0].cells[j])); + } + + cols.push(cache.normalized.length); // add position for rowCache + cache.normalized.push(cols); + cols = null; + }; + + if (table.config.debug) { + benchmark("Building cache for " + totalRows + " rows:", cacheTime); + } + + return cache; + }; + + function getElementText(config, node) { + + var text = ""; + + if (!node) return ""; + + if (!config.supportsTextContent) config.supportsTextContent = node.textContent || false; + + if (config.textExtraction == "simple") { + if (config.supportsTextContent) { + text = node.textContent; + } else { + if (node.childNodes[0] && node.childNodes[0].hasChildNodes()) { + text = node.childNodes[0].innerHTML; + } else { + text = node.innerHTML; + } + } + } else { + if (typeof(config.textExtraction) == "function") { + text = config.textExtraction(node); + } else { + text = $(node).text(); + } + } + return text; + } + + function appendToTable(table, cache) { + + if (table.config.debug) { + var appendTime = new Date() + } + + var c = cache, + r = c.row, + n = c.normalized, + totalRows = n.length, + checkCell = (n[0].length - 1), + tableBody = $(table.tBodies[0]), + rows = []; + + + for (var i = 0; i < totalRows; i++) { + var pos = n[i][checkCell]; + + rows.push(r[pos]); + + if (!table.config.appender) { + + //var o = ; + var l = r[pos].length; + for (var j = 0; j < l; j++) { + tableBody[0].appendChild(r[pos][j]); + } + + // + } + } + + + + if (table.config.appender) { + + table.config.appender(table, rows); + } + + rows = null; + + if (table.config.debug) { + benchmark("Rebuilt table:", appendTime); + } + + // apply table widgets + applyWidget(table); + + // trigger sortend + setTimeout(function () { + $(table).trigger("sortEnd"); + }, 0); + + }; + + function buildHeaders(table) { + + if (table.config.debug) { + var time = new Date(); + } + + var meta = ($.metadata) ? true : false; + + var header_index = computeTableHeaderCellIndexes(table); + + $tableHeaders = $(table.config.selectorHeaders, table).each(function (index) { + + this.column = header_index[this.parentNode.rowIndex + "-" + this.cellIndex]; + // this.column = index; + this.order = formatSortingOrder(table.config.sortInitialOrder); + + + this.count = this.order; + + if (checkHeaderMetadata(this) || checkHeaderOptions(table, index)) this.sortDisabled = true; + if (checkHeaderOptionsSortingLocked(table, index)) this.order = this.lockedOrder = checkHeaderOptionsSortingLocked(table, index); + + if (!this.sortDisabled) { + var $th = $(this).addClass(table.config.cssHeader); + if (table.config.onRenderHeader) table.config.onRenderHeader.apply($th); + } + + // add cell to headerList + table.config.headerList[index] = this; + }); + + if (table.config.debug) { + benchmark("Built headers:", time); + log($tableHeaders); + } + + return $tableHeaders; + + }; + + // from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + + + function computeTableHeaderCellIndexes(t) { + var matrix = []; + var lookup = {}; + var thead = t.getElementsByTagName('THEAD')[0]; + var trs = thead.getElementsByTagName('TR'); + + for (var i = 0; i < trs.length; i++) { + var cells = trs[i].cells; + for (var j = 0; j < cells.length; j++) { + var c = cells[j]; + + var rowIndex = c.parentNode.rowIndex; + var cellId = rowIndex + "-" + c.cellIndex; + var rowSpan = c.rowSpan || 1; + var colSpan = c.colSpan || 1 + var firstAvailCol; + if (typeof(matrix[rowIndex]) == "undefined") { + matrix[rowIndex] = []; + } + // Find first available column in the first row + for (var k = 0; k < matrix[rowIndex].length + 1; k++) { + if (typeof(matrix[rowIndex][k]) == "undefined") { + firstAvailCol = k; + break; + } + } + lookup[cellId] = firstAvailCol; + for (var k = rowIndex; k < rowIndex + rowSpan; k++) { + if (typeof(matrix[k]) == "undefined") { + matrix[k] = []; + } + var matrixrow = matrix[k]; + for (var l = firstAvailCol; l < firstAvailCol + colSpan; l++) { + matrixrow[l] = "x"; + } + } + } + } + return lookup; + } + + function checkCellColSpan(table, rows, row) { + var arr = [], + r = table.tHead.rows, + c = r[row].cells; + + for (var i = 0; i < c.length; i++) { + var cell = c[i]; + + if (cell.colSpan > 1) { + arr = arr.concat(checkCellColSpan(table, headerArr, row++)); + } else { + if (table.tHead.length == 1 || (cell.rowSpan > 1 || !r[row + 1])) { + arr.push(cell); + } + // headerArr[row] = (i+row); + } + } + return arr; + }; + + function checkHeaderMetadata(cell) { + if (($.metadata) && ($(cell).metadata().sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptions(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].sorter === false)) { + return true; + }; + return false; + } + + function checkHeaderOptionsSortingLocked(table, i) { + if ((table.config.headers[i]) && (table.config.headers[i].lockedOrder)) return table.config.headers[i].lockedOrder; + return false; + } + + function applyWidget(table) { + var c = table.config.widgets; + var l = c.length; + for (var i = 0; i < l; i++) { + + getWidgetById(c[i]).format(table); + } + + } + + function getWidgetById(name) { + var l = widgets.length; + for (var i = 0; i < l; i++) { + if (widgets[i].id.toLowerCase() == name.toLowerCase()) { + return widgets[i]; + } + } + }; + + function formatSortingOrder(v) { + if (typeof(v) != "Number") { + return (v.toLowerCase() == "desc") ? 1 : 0; + } else { + return (v == 1) ? 1 : 0; + } + } + + function isValueInArray(v, a) { + var l = a.length; + for (var i = 0; i < l; i++) { + if (a[i][0] == v) { + return true; + } + } + return false; + } + + function setHeadersCss(table, $headers, list, css) { + // remove all header information + $headers.removeClass(css[0]).removeClass(css[1]); + + var h = []; + $headers.each(function (offset) { + if (!this.sortDisabled) { + h[this.column] = $(this); + } + }); + + var l = list.length; + for (var i = 0; i < l; i++) { + h[list[i][0]].addClass(css[list[i][1]]); + } + } + + function fixColumnWidth(table, $headers) { + var c = table.config; + if (c.widthFixed) { + var colgroup = $('<colgroup>'); + $("tr:first td", table.tBodies[0]).each(function () { + colgroup.append($('<col>').css('width', $(this).width())); + }); + $(table).prepend(colgroup); + }; + } + + function updateHeaderSortCount(table, sortList) { + var c = table.config, + l = sortList.length; + for (var i = 0; i < l; i++) { + var s = sortList[i], + o = c.headerList[s[0]]; + o.count = s[1]; + o.count++; + } + } + + /* sorting methods */ + + function multisort(table, sortList, cache) { + + if (table.config.debug) { + var sortTime = new Date(); + } + + var dynamicExp = "var sortWrapper = function(a,b) {", + l = sortList.length; + + // TODO: inline functions. + for (var i = 0; i < l; i++) { + + var c = sortList[i][0]; + var order = sortList[i][1]; + // var s = (getCachedSortType(table.config.parsers,c) == "text") ? + // ((order == 0) ? "sortText" : "sortTextDesc") : ((order == 0) ? + // "sortNumeric" : "sortNumericDesc"); + // var s = (table.config.parsers[c].type == "text") ? ((order == 0) + // ? makeSortText(c) : makeSortTextDesc(c)) : ((order == 0) ? + // makeSortNumeric(c) : makeSortNumericDesc(c)); + var s = (table.config.parsers[c].type == "text") ? ((order == 0) ? makeSortFunction("text", "asc", c) : makeSortFunction("text", "desc", c)) : ((order == 0) ? makeSortFunction("numeric", "asc", c) : makeSortFunction("numeric", "desc", c)); + var e = "e" + i; + + dynamicExp += "var " + e + " = " + s; // + "(a[" + c + "],b[" + c + // + "]); "; + dynamicExp += "if(" + e + ") { return " + e + "; } "; + dynamicExp += "else { "; + + } + + // if value is the same keep orignal order + var orgOrderCol = cache.normalized[0].length - 1; + dynamicExp += "return a[" + orgOrderCol + "]-b[" + orgOrderCol + "];"; + + for (var i = 0; i < l; i++) { + dynamicExp += "}; "; + } + + dynamicExp += "return 0; "; + dynamicExp += "}; "; + + if (table.config.debug) { + benchmark("Evaling expression:" + dynamicExp, new Date()); + } + + eval(dynamicExp); + + cache.normalized.sort(sortWrapper); + + if (table.config.debug) { + benchmark("Sorting on " + sortList.toString() + " and dir " + order + " time:", sortTime); + } + + return cache; + }; + + function makeSortFunction(type, direction, index) { + var a = "a[" + index + "]", + b = "b[" + index + "]"; + if (type == 'text' && direction == 'asc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + a + " < " + b + ") ? -1 : 1 )));"; + } else if (type == 'text' && direction == 'desc') { + return "(" + a + " == " + b + " ? 0 : (" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : (" + b + " < " + a + ") ? -1 : 1 )));"; + } else if (type == 'numeric' && direction == 'asc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + a + " - " + b + "));"; + } else if (type == 'numeric' && direction == 'desc') { + return "(" + a + " === null && " + b + " === null) ? 0 :(" + a + " === null ? Number.POSITIVE_INFINITY : (" + b + " === null ? Number.NEGATIVE_INFINITY : " + b + " - " + a + "));"; + } + }; + + function makeSortText(i) { + return "((a[" + i + "] < b[" + i + "]) ? -1 : ((a[" + i + "] > b[" + i + "]) ? 1 : 0));"; + }; + + function makeSortTextDesc(i) { + return "((b[" + i + "] < a[" + i + "]) ? -1 : ((b[" + i + "] > a[" + i + "]) ? 1 : 0));"; + }; + + function makeSortNumeric(i) { + return "a[" + i + "]-b[" + i + "];"; + }; + + function makeSortNumericDesc(i) { + return "b[" + i + "]-a[" + i + "];"; + }; + + function sortText(a, b) { + if (table.config.sortLocaleCompare) return a.localeCompare(b); + return ((a < b) ? -1 : ((a > b) ? 1 : 0)); + }; + + function sortTextDesc(a, b) { + if (table.config.sortLocaleCompare) return b.localeCompare(a); + return ((b < a) ? -1 : ((b > a) ? 1 : 0)); + }; + + function sortNumeric(a, b) { + return a - b; + }; + + function sortNumericDesc(a, b) { + return b - a; + }; + + function getCachedSortType(parsers, i) { + return parsers[i].type; + }; /* public methods */ + this.construct = function (settings) { + return this.each(function () { + // if no thead or tbody quit. + if (!this.tHead || !this.tBodies) return; + // declare + var $this, $document, $headers, cache, config, shiftDown = 0, + sortOrder; + // new blank config object + this.config = {}; + // merge and extend. + config = $.extend(this.config, $.tablesorter.defaults, settings); + // store common expression for speed + $this = $(this); + // save the settings where they read + $.data(this, "tablesorter", config); + // build headers + $headers = buildHeaders(this); + // try to auto detect column type, and store in tables config + this.config.parsers = buildParserCache(this, $headers); + // build the cache for the tbody cells + cache = buildCache(this); + // get the css class names, could be done else where. + var sortCSS = [config.cssDesc, config.cssAsc]; + // fixate columns if the users supplies the fixedWidth option + fixColumnWidth(this); + // apply event handling to headers + // this is to big, perhaps break it out? + $headers.click( + + function (e) { + var totalRows = ($this[0].tBodies[0] && $this[0].tBodies[0].rows.length) || 0; + if (!this.sortDisabled && totalRows > 0) { + // Only call sortStart if sorting is + // enabled. + $this.trigger("sortStart"); + // store exp, for speed + var $cell = $(this); + // get current column index + var i = this.column; + // get current column sort order + this.order = this.count++ % 2; + // always sort on the locked order. + if(this.lockedOrder) this.order = this.lockedOrder; + + // user only whants to sort on one + // column + if (!e[config.sortMultiSortKey]) { + // flush the sort list + config.sortList = []; + if (config.sortForce != null) { + var a = config.sortForce; + for (var j = 0; j < a.length; j++) { + if (a[j][0] != i) { + config.sortList.push(a[j]); + } + } + } + // add column to sort list + config.sortList.push([i, this.order]); + // multi column sorting + } else { + // the user has clicked on an all + // ready sortet column. + if (isValueInArray(i, config.sortList)) { + // revers the sorting direction + // for all tables. + for (var j = 0; j < config.sortList.length; j++) { + var s = config.sortList[j], + o = config.headerList[s[0]]; + if (s[0] == i) { + o.count = s[1]; + o.count++; + s[1] = o.count % 2; + } + } + } else { + // add column to sort list array + config.sortList.push([i, this.order]); + } + }; + setTimeout(function () { + // set css for headers + setHeadersCss($this[0], $headers, config.sortList, sortCSS); + appendToTable( + $this[0], multisort( + $this[0], config.sortList, cache) + ); + }, 1); + // stop normal event by returning false + return false; + } + // cancel selection + }).mousedown(function () { + if (config.cancelSelection) { + this.onselectstart = function () { + return false + }; + return false; + } + }); + // apply easy methods that trigger binded events + $this.bind("update", function () { + var me = this; + setTimeout(function () { + // rebuild parsers. + me.config.parsers = buildParserCache( + me, $headers); + // rebuild the cache map + cache = buildCache(me); + }, 1); + }).bind("updateCell", function (e, cell) { + var config = this.config; + // get position from the dom. + var pos = [(cell.parentNode.rowIndex - 1), cell.cellIndex]; + // update cache + cache.normalized[pos[0]][pos[1]] = config.parsers[pos[1]].format( + getElementText(config, cell), cell); + }).bind("sorton", function (e, list) { + $(this).trigger("sortStart"); + config.sortList = list; + // update and store the sortlist + var sortList = config.sortList; + // update header count index + updateHeaderSortCount(this, sortList); + // set css for headers + setHeadersCss(this, $headers, sortList, sortCSS); + // sort the table and append it to the dom + appendToTable(this, multisort(this, sortList, cache)); + }).bind("appendCache", function () { + appendToTable(this, cache); + }).bind("applyWidgetId", function (e, id) { + getWidgetById(id).format(this); + }).bind("applyWidgets", function () { + // apply widgets + applyWidget(this); + }); + if ($.metadata && ($(this).metadata() && $(this).metadata().sortlist)) { + config.sortList = $(this).metadata().sortlist; + } + // if user has supplied a sort list to constructor. + if (config.sortList.length > 0) { + $this.trigger("sorton", [config.sortList]); + } + // apply widgets + applyWidget(this); + }); + }; + this.addParser = function (parser) { + var l = parsers.length, + a = true; + for (var i = 0; i < l; i++) { + if (parsers[i].id.toLowerCase() == parser.id.toLowerCase()) { + a = false; + } + } + if (a) { + parsers.push(parser); + }; + }; + this.addWidget = function (widget) { + widgets.push(widget); + }; + this.formatFloat = function (s) { + var i = parseFloat(s); + return (isNaN(i)) ? 0 : i; + }; + this.formatInt = function (s) { + var i = parseInt(s); + return (isNaN(i)) ? 0 : i; + }; + this.isDigit = function (s, config) { + // replace all an wanted chars and match. + return /^[-+]?\d*$/.test($.trim(s.replace(/[,.']/g, ''))); + }; + this.clearTableBody = function (table) { + if ($.browser.msie) { + function empty() { + while (this.firstChild) + this.removeChild(this.firstChild); + } + empty.apply(table.tBodies[0]); + } else { + table.tBodies[0].innerHTML = ""; + } + }; + } + }); + + // extend plugin scope + $.fn.extend({ + tablesorter: $.tablesorter.construct + }); + + // make shortcut + var ts = $.tablesorter; + + // add default parsers + ts.addParser({ + id: "text", + is: function (s) { + return true; + }, format: function (s) { + return $.trim(s.toLocaleLowerCase()); + }, type: "text" + }); + + ts.addParser({ + id: "digit", + is: function (s, table) { + var c = table.config; + return $.tablesorter.isDigit(s, c); + }, format: function (s) { + return $.tablesorter.formatFloat(s); + }, type: "numeric" + }); + + ts.addParser({ + id: "currency", + is: function (s) { + return /^[£$€?.]/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/[£$€]/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "ipAddress", + is: function (s) { + return /^\d{2,3}[.]\d{2,3}[.]\d{2,3}[.]\d{2,3}$/.test(s); + }, format: function (s) { + var a = s.split("."), + r = "", + l = a.length; + for (var i = 0; i < l; i++) { + var item = a[i]; + if (item.length == 2) { + r += "0" + item; + } else { + r += item; + } + } + return $.tablesorter.formatFloat(r); + }, type: "numeric" + }); + + ts.addParser({ + id: "url", + is: function (s) { + return /^(https?|ftp|file)://$/.test(s); + }, format: function (s) { + return jQuery.trim(s.replace(new RegExp(/(https?|ftp|file):///), '')); + }, type: "text" + }); + + ts.addParser({ + id: "isoDate", + is: function (s) { + return /^\d{4}[/-]\d{1,2}[/-]\d{1,2}$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat((s != "") ? new Date(s.replace( + new RegExp(/-/g), "/")).getTime() : "0"); + }, type: "numeric" + }); + + ts.addParser({ + id: "percent", + is: function (s) { + return /%$/.test($.trim(s)); + }, format: function (s) { + return $.tablesorter.formatFloat(s.replace(new RegExp(/%/g), "")); + }, type: "numeric" + }); + + ts.addParser({ + id: "usLongDate", + is: function (s) { + return s.match(new RegExp(/^[A-Za-z]{3,10}.? [0-9]{1,2}, ([0-9]{4}|'?[0-9]{2}) (([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(AM|PM)))$/)); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + + ts.addParser({ + id: "shortDate", + is: function (s) { + return /\d{1,2}[/-]\d{1,2}[/-]\d{2,4}/.test(s); + }, format: function (s, table) { + var c = table.config; + s = s.replace(/-/g, "/"); + if (c.dateFormat == "us") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/, "$3/$1/$2"); + } else if (c.dateFormat == "uk") { + // reformat the string in ISO format + s = s.replace(/(\d{1,2})[/-](\d{1,2})[/-](\d{4})/, "$3/$2/$1"); + } else if (c.dateFormat == "dd/mm/yy" || c.dateFormat == "dd-mm-yy") { + s = s.replace(/(\d{1,2})[/-](\d{1,2})[/-](\d{2})/, "$1/$2/$3"); + } + return $.tablesorter.formatFloat(new Date(s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "time", + is: function (s) { + return /^(([0-2]?[0-9]:[0-5][0-9])|([0-1]?[0-9]:[0-5][0-9]\s(am|pm)))$/.test(s); + }, format: function (s) { + return $.tablesorter.formatFloat(new Date("2000/01/01 " + s).getTime()); + }, type: "numeric" + }); + ts.addParser({ + id: "metadata", + is: function (s) { + return false; + }, format: function (s, table, cell) { + var c = table.config, + p = (!c.parserMetadataName) ? 'sortValue' : c.parserMetadataName; + return $(cell).metadata()[p]; + }, type: "numeric" + }); + // add default widgets + ts.addWidget({ + id: "zebra", + format: function (table) { + if (table.config.debug) { + var time = new Date(); + } + var $tr, row = -1, + odd; + // loop through the visible rows + $("tr:visible", table.tBodies[0]).each(function (i) { + $tr = $(this); + // style children rows the same way the parent + // row was styled + if (!$tr.hasClass(table.config.cssChildRow)) row++; + odd = (row % 2 == 0); + $tr.removeClass( + table.config.widgetZebra.css[odd ? 0 : 1]).addClass( + table.config.widgetZebra.css[odd ? 1 : 0]) + }); + if (table.config.debug) { + $.tablesorter.benchmark("Applying Zebra widget", time); + } + } + }); +})(jQuery); \ No newline at end of file diff --git a/js/messages.php b/js/messages.php index b2209cf..a09078e 100644 --- a/js/messages.php +++ b/js/messages.php @@ -46,6 +46,10 @@ $js_messages['strBLOBRepositoryDisableAreYouSure'] = sprintf(__('Are you sure yo $js_messages['strFormEmpty'] = __('Missing value in the form!'); $js_messages['strNotNumber'] = __('This is not a number!');
+/* Charts */ +/* l10n: Default description for the y-Axis of Charts */ +$js_messages['strTotalCount'] = __('Total count'); + /* For server_privileges.js */ $js_messages['strHostEmpty'] = __('The host name is empty!'); $js_messages['strUserEmpty'] = __('The user name is empty!'); @@ -56,6 +60,18 @@ $js_messages['strReloadingPrivileges'] = __('Reloading Privileges'); $js_messages['strRemovingSelectedUsers'] = __('Removing Selected Users'); $js_messages['strClose'] = __('Close');
+/* for server_status.js */ +$js_messages['strRealtimeChart'] = __('Realtime chart'); +$js_messages['strStaticData'] = __('Static data'); +/* l10n: Total number of queries */ +$js_messages['strTotal'] = __('Total'); +/* l10n: Other, small valued, queries */ +$js_messages['strOther'] = __('Other'); +/* l10n: Thousands separator */ +$js_messages['strThousandsSeperator'] = __(','); +/* l10n: Decimal separator */ +$js_messages['strDecimalSeperator'] = __('.'); + /* For inline query editing */ $js_messages['strGo'] = __('Go'); $js_messages['strCancel'] = __('Cancel'); diff --git a/js/pMap.js b/js/pMap.js deleted file mode 100644 index b63221c..0000000 --- a/js/pMap.js +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Holds the definition and the creation of the imageMap object - * @author Martynas Mickevicius mmartynas@gmail.com - * @package phpMyAdmin - */ - -/** - * responsible for showing tooltips above the image chart - */ -var imageMap = { - 'mouseMoved': function(event, cont) { - // return if no imageMap set - // this can happen if server has no json - if (!this.imageMap) { - return; - } - - // get mouse coordinated relative to image - var mouseX = event.pageX - cont.offsetLeft; - var mouseY = event.pageY - cont.offsetTop; - - //console.log("X: " + mouseX + ", Y: " + mouseY); - - /* Check if we are flying over a map zone - * Lets use the following method to check if a given - * point is in any convex polygon. - * http://www.programmingforums.org/post168124-3.html - */ - var found = false; - for (var key = 0; key < this.imageMap.length; key++) - { - var seriesName = this.imageMap[key]['n']; - var seriesValue = this.imageMap[key]['v']; - - var signSum = 0; - for (var i = 0; i < this.imageMap[key]['p'].length; i++) - { - var index1; - var index2; - - if (i == this.imageMap[key]['p'].length - 1) - { - index1 = i; - index2 = 0; - } - else - { - index1 = i; - index2 = i+1; - } - var result = this.getDeterminant( - this.imageMap[key]['p'][index1][0], - this.imageMap[key]['p'][index1][1], - this.imageMap[key]['p'][index2][0], - this.imageMap[key]['p'][index2][1], - mouseX, - mouseY - ); - if (result > 0) { signSum += 1; } else { signSum += -1; } - } - - if (Math.abs(signSum) == this.imageMap[key]['p'].length) - { - found = true; - if (this.currentKey != key) - { - this.tooltip.show(); - this.tooltip.title(seriesName); - this.tooltip.text(seriesValue); - this.currentKey = key; - } - this.tooltip.move(mouseX + 20, mouseY + 20); - } - } - if (!found && this.currentKey != -1 ) - { - this.tooltip.hide(); - this.currentKey = -1; - } - }, - - 'getDeterminant': function (X1, Y1, X2, Y2, X3, Y3) { - return (X2*Y3 - X3*Y2) - (X1*Y3 - X3*Y1) + (X1*Y2 - X2*Y1); - }, - - 'loadImageMap': function(map) { - this.imageMap = JSON.parse(map); - for (key in this.imageMap) - { - // FIXME - // without this loop image map does not work - // on IE8 in the status page - } - }, - - 'init': function() { - this.tooltip.init(); - - $("div#chart").bind('mousemove',function(e) { - imageMap.mouseMoved(e, this); - }); - - this.tooltip.attach("div#chart"); - - this.currentKey = -1; - }, - - 'tooltip': { - 'init': function () { - this.el = $('<div></div>'); - this.el.css('position', 'absolute'); - this.el.css('font-family', 'tahoma'); - this.el.css('background-color', '#373737'); - this.el.css('color', '#BEBEBE'); - this.el.css('padding', '3px'); - - var title = $('<p></p>'); - title.attr('id', 'title'); - title.css('margin', '0px'); - title.css('padding', '3px'); - title.css('background-color', '#606060'); - title.css('text-align', 'center'); - title.html('Title'); - this.el.append(title); - - var text = $('<p></p>'); - text.attr('id', 'text'); - text.css('margin', '0'); - text.html('Text'); - this.el.append(text); - - this.hide(); - }, - - 'attach': function (element) { - $(element).prepend(this.el); - }, - - 'move': function (x, y) { - this.el.css('margin-left', x); - this.el.css('margin-top', y); - }, - - 'hide': function () { - this.el.css('display', 'none'); - }, - - 'show': function () { - this.el.css('display', 'block'); - }, - - 'title': function (title) { - this.el.find("p#title").html(title); - }, - - 'text': function (text) { - this.el.find("p#text").html(text.replace(/;/g, "<br />")); - } - } -}; - -$(document).ready(function() { - imageMap.init(); -}); diff --git a/js/server_status.js b/js/server_status.js new file mode 100644 index 0000000..75894ad --- /dev/null +++ b/js/server_status.js @@ -0,0 +1,373 @@ +/* vim: set expandtab sw=4 ts=4 sts=4: */ +/** + * @fileoverview functions used in server status pages + * @name Server Status + * + * @requires jQuery + * @requires jQueryUI + * @requires jQueryCookie + * @requires jQueryTablesorter + * @requires Highcharts + * @requires canvg + * @requires js/functions.js + * + */ + +// Add a tablesorter parser to properly handle thousands seperated numbers and SI prefixes +$(function() { + jQuery.tablesorter.addParser({ + id: "fancyNumber", + is: function(s) { + return /^[0-9]?[0-9,.]*\s?(k|M|G|T|%)?$/.test(s); + }, + format: function(s) { + var num = jQuery.tablesorter.formatFloat( s.replace(PMA_messages['strThousandsSeperator'],'').replace(PMA_messages['strDecimalSeperator'],'.') ); + var factor = 1; + switch (s.charAt(s.length-1)) { + case '%': factor = -2; break; + // Todo: Complete this list (as well as in the regexp a few lines up) + case 'k': factor = 3; break; + case 'M': factor = 6; break; + case 'G': factor = 9; break; + case 'T': factor = 12; break; + } + return num*Math.pow(10,factor); + }, + type: "numeric" + }); +}); + +$(function() { + // Filters for status variables + var textFilter=null; + var alertFilter = false; + var categoryFilter=''; + var odd_row=false; + var text=''; // Holds filter text + var queryPieChart = null; + /* Chart configuration */ + + // Defines what the tabs are currently displaying (realtime or data) + var tabStatus = new Object(); + // Holds the current chart instances for each tab + var tabChart = new Object(); + + // Add tabs + $('#serverStatusTabs').tabs({ + // Tab persistence + cookie: { name: 'pma_serverStatusTabs', expires: 1 }, + // Fixes line break in the menu bar when the page overflows and scrollbar appears + show: function() { menuResize(); } + }); + + // Fixes wrong tab height with floated elements. See also http://bugs.jqueryui.com/ticket/5601 + $(".ui-widget-content:not(.ui-tabs):not(.ui-helper-clearfix)").addClass("ui-helper-clearfix"); + + // Initialize each tab + $('div.ui-tabs-panel').each(function() { initTab($(this),null); }); + + $('.statuslinks select').change(function() { + var chart=tabChart[$(this).parents('div.ui-tabs-panel').attr('id')]; + chart.options.realtime.refreshRate = 1000*parseInt(this.value); + chart.xAxis[0].setExtremes(new Date().getTime() - chart.options.realtime.numMaxPoints * chart.options.realtime.refreshRate, chart.xAxis[0].getExtremes().max, true); + clearTimeout(chart_activeTimeouts[chart.options.chart.renderTo]); + chart_activeTimeouts[chart.options.chart.renderTo] = setTimeout(chart.options.realtime.timeoutCallBack, chart.options.realtime.refreshRate); + }); + + // Ajax refresh of variables (always the first element in each tab) + $('.statuslinks a.tabRefresh').click(function() { + // ui-tabs-panel class is added by the jquery tabs feature + var tab=$(this).parents('div.ui-tabs-panel'); + var that = this; + + // Show ajax load icon + $(this).find('img').show(); + + $.get($(this).attr('href'),{ajax_request:1},function(data) { + $(that).find('img').hide(); + initTab(tab,data); + }); + + tabStatus[tab.attr('id')]='data'; + + return false; + }); + + /** Realtime charting of variables (always the third element) **/ + $('.statuslinks a.tabChart').click(function() { + // ui-tabs-panel class is added by the jquery tabs feature + var tab=$(this).parents('div.ui-tabs-panel'); + + if(tabStatus[tab.attr('id')]!='realtime') { + var series, title; + var settings = new Object(); + + switch(tab.attr('id')) { + case 'statustabs_traffic': + settings = { + series: [{name:'Connections since last refresh', data:[]},{name:'Processes', data:[]}], + title: {text:'Connections / Processes'}, + realtime:{ url:'server_status.php?'+url_query, + type: 'proc', + callback: function(chartObj, curVal, lastVal,numLoadedPoints) { + if(lastVal==null) return; + chartObj.series[0].addPoint( + { x:curVal.x, y:curVal.y_conn-lastVal.y_conn }, + false, numLoadedPoints >= chartObj.options.realtime.numMaxPoints + ); + chartObj.series[1].addPoint( + { x:curVal.x, y:curVal.y_proc }, + true, numLoadedPoints >= chartObj.options.realtime.numMaxPoints + ); + } + } + }; + break; + case 'statustabs_queries': + settings = { + series: [{name:'Issued queries since last refresh', data:[]}], + title: {text:'Issued queries'}, + tooltip: { formatter:function() { return this.point.name; } }, + realtime:{ url:'server_status.php?'+url_query, + type: 'queries', + callback: function(chartObj, curVal, lastVal,numLoadedPoints) { + if(lastVal==null) return; + chartObj.series[0].addPoint( + { x:curVal.x, y:curVal.y-lastVal.y, name:sortedQueriesPointInfo(curVal,lastVal) }, + true, numLoadedPoints >= chartObj.options.realtime.numMaxPoints + ); + } + } + }; + break; + + default: + return; + } + + if(!settings.chart) settings.chart = {}; + settings.chart.renderTo=tab.attr('id')+"_chart_cnt"; + + tab.find('.tabInnerContent') + .hide() + .after('<div style="clear:both; min-width:500px; height:400px; padding-bottom:80px;" id="'+tab.attr('id')+'_chart_cnt"></div>'); + tabStatus[tab.attr('id')]='realtime'; + tabChart[tab.attr('id')]=PMA_createChart(settings); + $(this).html(PMA_messages['strStaticData']); + tab.find('.statuslinks a.tabRefresh').hide(); + tab.find('.statuslinks select').show(); + } else { + clearTimeout(chart_activeTimeouts[tab.attr('id')+"_chart_cnt"]); + chart_activeTimeouts[tab.attr('id')+"_chart_cnt"]=null; + tab.find('.tabInnerContent').show(); + tab.find('div#'+tab.attr('id')+'_chart_cnt').remove(); + tabStatus[tab.attr('id')]='data'; + tabChart[tab.attr('id')].destroy(); + $(this).html(PMA_messages['strRealtimeChart']); + tab.find('.statuslinks a.tabRefresh').show(); + tab.find('.statuslinks select').hide(); + } + return false; + }); + + + /* 3 Filtering functions */ + $('#filterAlert').change(function() { + alertFilter = this.checked; + filterVariables(); + }); + + $('#filterText').keyup(function(e) { + if($(this).val().length==0) textFilter=null; + else textFilter = new RegExp("(^|_)"+$(this).val(),'i'); + text=$(this).val(); + filterVariables(); + }); + + $('#filterCategory').change(function() { + categoryFilter = $(this).val(); + filterVariables(); + }); + + /* Adjust DOM / Add handlers to the tabs */ + function initTab(tab,data) { + switch(tab.attr('id')) { + case 'statustabs_traffic': + if(data!=null) tab.find('.tabInnerContent').html(data); + initTooltips(); + break; + case 'statustabs_queries': + if(data!=null) { + queryPieChart.destroy(); + tab.find('.tabInnerContent').html(data); + } + + // Build query statistics chart + var cdata = new Array(); + $.each(jQuery.parseJSON($('#serverstatusquerieschart').html()),function(key,value) { + cdata.push([key,parseInt(value)]); + }); + + queryPieChart=PMA_createChart({ + chart: { + renderTo: 'serverstatusquerieschart' + + }, + title: { + text:'', + margin:0 + }, + series: [{ + type:'pie', + name: 'Query statistics', + data: cdata + }], + plotOptions: { + pie: { + allowPointSelect: true, + cursor: 'pointer', + dataLabels: { + enabled: true, + formatter: function() { + return '<b>'+ this.point.name +'</b><br> '+ Highcharts.numberFormat(this.percentage, 2) +' %'; + } + } + } + }, + tooltip: { + formatter: function() { return '<b>'+ this.point.name +'</b><br/>'+Highcharts.numberFormat(this.y, 2)+'<br/>('+Highcharts.numberFormat(this.percentage, 2) +' %)'; } + } + }); + break; + + case 'statustabs_allvars': + if(data!=null) { + tab.find('.tabInnerContent').html(data); + filterVariables(); + } + break; + } + + initTableSorter(tab.attr('id')); + } + + function initTableSorter(tabid) { + switch(tabid) { + case 'statustabs_queries': + $('#serverstatusqueriesdetails').tablesorter({ + sortList: [[3,1]], + widgets: ['zebra'], + headers: { + 1: { sorter: 'fancyNumber' }, + 2: { sorter: 'fancyNumber' } + } + }); + + $('#serverstatusqueriesdetails tr:first th') + .append('<img class="sortableIcon" src="'+pma_theme_image+'cleardot.gif" alt="">'); + + break; + + case 'statustabs_allvars': + $('#serverstatusvariables').tablesorter({ + sortList: [[0,0]], + widgets: ['zebra'], + headers: { + 1: { sorter: 'fancyNumber' } + } + }); + + $('#serverstatusvariables tr:first th') + .append('<img class="sortableIcon" src="'+pma_theme_image+'cleardot.gif" alt="">'); + + break; + } + } + + /* Filters the status variables by name/category/alert in the variables tab */ + function filterVariables() { + var useful_links=0; + var section = text; + + if(categoryFilter.length>0) section = categoryFilter; + + if(section.length>1) { + $('#linkSuggestions span').each(function() { + if($(this).attr('class').indexOf('status_'+section)!=-1) { + useful_links++; + $(this).css('display',''); + } else { + $(this).css('display','none'); + } + + + }); + } + + if(useful_links>0) + $('#linkSuggestions').css('display',''); + else $('#linkSuggestions').css('display','none'); + + odd_row=false; + $('#serverstatusvariables th.name').each(function() { + if((textFilter==null || textFilter.exec($(this).text())) + && (!alertFilter || $(this).next().find('span.attention').length>0) + && (categoryFilter.length==0 || $(this).parent().hasClass('s_'+categoryFilter))) { + odd_row = !odd_row; + $(this).parent().css('display',''); + if(odd_row) { + $(this).parent().addClass('odd'); + $(this).parent().removeClass('even'); + } else { + $(this).parent().addClass('even'); + $(this).parent().removeClass('odd'); + } + } else { + $(this).parent().css('display','none'); + } + }); + } + + // Provides a nicely formatted and sorted tooltip of each datapoint of the query statistics + function sortedQueriesPointInfo(queries, lastQueries){ + var max, maxIdx, num=0; + var queryKeys = new Array(); + var queryValues = new Array(); + var sumOther=0; + var sumTotal=0; + + // Separate keys and values, then sort them + $.each(queries.pointInfo, function(key,value) { + if(value-lastQueries.pointInfo[key] > 0) { + queryKeys.push(key); + queryValues.push(value-lastQueries.pointInfo[key]); + sumTotal+=value-lastQueries.pointInfo[key]; + } + }); + var numQueries = queryKeys.length; + var pointInfo = '<b>' + PMA_messages['strTotal'] + ': ' + sumTotal + '</b><br>'; + + while(queryKeys.length > 0) { + max=0; + for(var i=0; i<queryKeys.length; i++) { + if(queryValues[i] > max) { + max = queryValues[i]; + maxIdx = i; + } + } + if(numQueries > 8 && num>=6) + sumOther+=queryValues[maxIdx]; + else pointInfo += queryKeys[maxIdx].substr(4).replace('_',' ') + ': ' + queryValues[maxIdx] + '<br>'; + + queryKeys.splice(maxIdx,1); + queryValues.splice(maxIdx,1); + num++; + } + + if(sumOther>0) + pointInfo += PMA_messages['strOther'] + ': ' + sumOther; + + return pointInfo; + } + +}); \ No newline at end of file diff --git a/js/server_variables.js b/js/server_variables.js new file mode 100644 index 0000000..1877ce7 --- /dev/null +++ b/js/server_variables.js @@ -0,0 +1,42 @@ +$(function() {
+ var textFilter=null;
+ var odd_row=false;
+
+ // Filter options are invisible for disabled js users
+ $('fieldset#tableFilter').css('display','');
+
+ $('#filterText').keyup(function(e) {
+ if($(this).val().length==0) textFilter=null;
+ else textFilter = new RegExp("(^| )"+$(this).val(),'i');
+ filterVariables();
+ });
+
+ function filterVariables() {
+ odd_row=false;
+ var mark_next=false;
+ var firstCell;
+
+ $('table.filteredData tbody tr').each(function() {
+ firstCell = $(this).children(':first');
+
+ if(mark_next || textFilter==null || textFilter.exec(firstCell.text())) {
+ // If current row is 'marked', also display next row
+ if($(this).hasClass('marked') && !mark_next)
+ mark_next=true;
+ else mark_next=false;
+
+ odd_row = !odd_row;
+ $(this).css('display','');
+ if(odd_row) {
+ $(this).addClass('odd');
+ $(this).removeClass('even');
+ } else {
+ $(this).addClass('even');
+ $(this).removeClass('odd');
+ }
+ } else {
+ $(this).css('display','none');
+ }
+ });
+ }
+}); \ No newline at end of file diff --git a/js/sql.js b/js/sql.js index 3bc4f81..65d209a 100644 --- a/js/sql.js +++ b/js/sql.js @@ -1155,6 +1155,51 @@ $(document).ready(function() { $('.column_heading.marker').live('click', function() { PMA_changeClassForColumn($(this), 'marked'); }); -}) +}); + +/* + * Profiling Chart + */ +function createProfilingChart() { + if($('#profilingchart').length==0) return; + + var cdata = new Array(); + $.each(jQuery.parseJSON($('#profilingchart').html()),function(key,value) { + cdata.push([key,parseFloat(value)]); + }); + + // Prevent the user from seeing the JSON code + $('div#profilingchart').html('').show(); + + PMA_createChart({ + chart: { + renderTo: 'profilingchart', + backgroundColor: $('#sqlqueryresults fieldset').css('background-color') + }, + title: { text:'', margin:0 }, + series: [{ + type:'pie', + name: 'Query execution time', + data: cdata + }], + plotOptions: { + pie: { + allowPointSelect: true, + cursor: 'pointer', + dataLabels: { + enabled: true, + distance: 35, + formatter: function() { + return '<b>'+ this.point.name +'</b><br/>'+ Highcharts.numberFormat(this.percentage, 2) +' %'; + } + } + } + }, + tooltip: { + formatter: function() { return '<b>'+ this.point.name +'</b><br/>'+this.y+'s<br/>('+Highcharts.numberFormat(this.percentage, 2) +' %)'; } + } + }); +} +
/**#@- */ diff --git a/js/tbl_chart.js b/js/tbl_chart.js new file mode 100644 index 0000000..7c870d0 --- /dev/null +++ b/js/tbl_chart.js @@ -0,0 +1,236 @@ +var chart_xaxis_idx = -1; +var chart_series; +var chart_series_index = -1; + +$(document).ready(function() { + var currentChart=null; + var chart_data = jQuery.parseJSON($('#querychart').html()); + chart_series = 'columns'; + chart_xaxis_idx = $('select[name="chartXAxis"]').attr('value'); + + $('#resizer').resizable({ + minHeight:240, + minWidth:300, + // On resize, set the chart size to that of the + // resizer minus padding. If your chart has a lot of data or other + // content, the redrawing might be slow. In that case, we recommend + // that you use the 'stop' event instead of 'resize'. + resize: function() { + currentChart.setSize( + this.offsetWidth - 20, + this.offsetHeight - 20, + false + ); + } + }); + + var currentSettings = { + chart: { + type:'line', + width:$('#resizer').width()-20, + height:$('#resizer').height()-20 + }, + xAxis: { + title: { text: $('input[name="xaxis_label"]').attr('value') } + }, + yAxis: { + title: { text: $('input[name="yaxis_label"]').attr('value') } + }, + title: { text: $('input[name="chartTitle"]').attr('value'), margin:20 }, + plotOptions: { + series: {} + } + } + + $('#querychart').html(''); + + $('input[name="chartType"]').click(function() { + currentSettings.chart.type=$(this).attr('value'); + + drawChart(); + + if($(this).attr('value')=='bar' || $(this).attr('value')=='column') + $('span.barStacked').show(); + else + $('span.barStacked').hide(); + }); + + $('input[name="barStacked"]').click(function() { + if(this.checked) + $.extend(true,currentSettings,{ plotOptions: { series: { stacking:'normal' } } }); + else + $.extend(true,currentSettings,{ plotOptions: { series: { stacking:null } } }); + drawChart(); + }); + + $('input[name="chartTitle"]').keyup(function() { + var title=$(this).attr('value'); + if(title.length==0) title=' '; + currentChart.setTitle({text: title}); + }); + + $('select[name="chartXAxis"]').change(function() { + chart_xaxis_idx = this.value; + drawChart(); + }); + $('select[name="chartSeries"]').change(function() { + chart_series = this.value; + chart_series_index = this.selectedIndex; + drawChart(); + }); + + /* Sucks, we cannot just set axis labels, we have to redraw the chart completely */ + $('input[name="xaxis_label"]').keyup(function() { + currentSettings.xAxis.title.text = $(this).attr('value'); + drawChart(true); + }); + $('input[name="yaxis_label"]').keyup(function() { + currentSettings.yAxis.title.text = $(this).attr('value'); + drawChart(true); + }); + + function drawChart(noAnimation) { + currentSettings.chart.width=$('#resizer').width()-20; + currentSettings.chart.height=$('#resizer').height()-20; + + if(currentChart!=null) currentChart.destroy(); + + if(noAnimation) currentSettings.plotOptions.series.animation = false; + currentChart = PMA_queryChart(chart_data,currentSettings); + if(noAnimation) currentSettings.plotOptions.series.animation = true; + } + + drawChart(); + $('#querychart').show(); +}); + +function in_array(element,array) { + for(var i=0; i<array.length; i++) + if(array[i]==element) return true; + return false; +} + +function PMA_queryChart(data,passedSettings) { + if($('#querychart').length==0) return; + + var columnNames = Array(); + + var series = new Array(); + var xaxis = new Object(); + var yaxis = new Object(); + + $.each(data[0],function(index,element) { + columnNames.push(index); + }); + + switch(passedSettings.chart.type) { + case 'column': + case 'spline': + case 'line': + case 'bar': + xaxis.categories = new Array(); + + if(chart_series=='columns') { + var j=0; + for(var i=0; i<columnNames.length; i++) + if(i!=chart_xaxis_idx) { + series[j] = new Object(); + series[j].data = new Array(); + series[j].name = columnNames[i]; + $.each(data,function(key,value) { + series[j].data.push(parseFloat(value[columnNames[i]])); + if(j==0 && chart_xaxis_idx!=-1 && !xaxis.categories[value[columnNames[chart_xaxis_idx]]]) + xaxis.categories.push(value[columnNames[chart_xaxis_idx]]); + }); + j++; + } + } else { + var j=0; + var seriesIndex = new Object(); + // Get series types and build series object from the query data + $.each(data,function(index,element) { + var contains=false; + for(var i=0; i<series.length; i++) + if(series[i].name == element[chart_series]) contains=true; + + if(!contains) { + seriesIndex[element[chart_series]] = j; + series[j] = new Object(); + series[j].data = new Array(); + series[j].name = element[chart_series]; // columnNames[i]; + j++; + } + }); + + var type; + // Get series points from query data + $.each(data,function(key,value) { + type = value[chart_series]; + series[seriesIndex[type]].data.push(parseFloat(value[columnNames[0]])); + if(!in_array(value[columnNames[chart_xaxis_idx]],xaxis.categories)) + xaxis.categories.push(value[columnNames[chart_xaxis_idx]]); + }); + } + + + + if(columnNames.length==2) + yaxis.title = { text: columnNames[0] }; + break; + + case 'pie': + series[0] = new Object(); + series[0].data = new Array(); + $.each(data,function(key,value) { + series[0].data.push({name:value[columnNames[chart_xaxis_idx]],y:parseFloat(value[columnNames[0]])}); + }); + break; + } + + // Prevent the user from seeing the JSON code + $('div#profilingchart').html('').show(); + + var settings = { + chart: { + renderTo: 'querychart', + backgroundColor: $('fieldset').css('background-color') + }, + title: { text:'', margin:0 }, + series: series, + xAxis: xaxis, + yAxis: yaxis, + plotOptions: { + pie: { + allowPointSelect: true, + cursor: 'pointer', + dataLabels: { + enabled: true, + distance: 35, + formatter: function() { + return '<b>'+ this.point.name +'</b><br/>'+ Highcharts.numberFormat(this.percentage, 2) +' %'; + } + } + } + }, + credits: { + enabled:false + }, + exporting: { + enabled: true + }, + tooltip: { + formatter: function() { + if(this.point.name) return '<b>'+this.series.name+'</b><br/>'+this.point.name+'<br/>'+this.y; + return '<b>'+this.series.name+'</b><br/>'+this.y; + } + } + }; + + if(passedSettings.chart.type=='pie') + settings.tooltip.formatter = function() { return '<b>'+columnNames[0]+'</b><br/>'+this.y; } + + // Overwrite/Merge default settings with passedsettings + $.extend(true,settings,passedSettings); + + return new Highcharts.Chart(settings); +} diff --git a/libraries/chart.lib.php b/libraries/chart.lib.php deleted file mode 100644 index 83791fb..0000000 --- a/libraries/chart.lib.php +++ /dev/null @@ -1,256 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * Chart functions used to generate various types of charts. - * @package phpMyAdmin - */ - -/** - * - */ -define('ERR_NO_GD', 0); -define('ERR_NO_JSON', 1); - -require_once './libraries/chart/pma_pchart_pie.php'; -require_once './libraries/chart/pma_pchart_single_bar.php'; -require_once './libraries/chart/pma_pchart_multi_bar.php'; -require_once './libraries/chart/pma_pchart_stacked_bar.php'; -require_once './libraries/chart/pma_pchart_single_line.php'; -require_once './libraries/chart/pma_pchart_multi_line.php'; -require_once './libraries/chart/pma_pchart_single_radar.php'; -require_once './libraries/chart/pma_pchart_multi_radar.php'; - -/** - * Formats a chart for the status page. - * @param array $data data for the status chart - * @return string HTML and JS code for the chart - */ -function PMA_chart_status($data) -{ - // format keys which will be shown in the chart - $chartData = array(); - foreach($data as $dataKey => $dataValue) { - $key = ucwords(str_replace(array('Com_', '_'), array('', ' '), $dataKey)); - $value = (int)$dataValue; - $chartData[$key] = $value; - } - - $chart = new PMA_pChart_Pie( - $chartData, - array('titleText' => __('Query statistics')) - ); - $chartCode = $chart->toString(); - PMA_handle_chart_err($chart->getErrors()); - echo $chartCode; -} - -/** - * Formats a chart for the profiling page. - * @param array $data data for the status chart - * @return string HTML and JS code for the chart - */ -function PMA_chart_profiling($data) -{ - $chartData = array(); - foreach($data as $dataValue) { - $value = (int)($dataValue['Duration'] * 1000000); - $key = ucwords($dataValue['Status']); - $chartData[$key] = $value; - } - - $chart = new PMA_pChart_Pie( - $chartData, - array('titleText' => __('Query execution time comparison (in microseconds)')) - ); - $chartCode = $chart->toString(); - PMA_handle_chart_err($chart->getErrors()); - echo $chartCode; -} - -/** - * Formats a chart for the query results page. - * @param array $data data for the status chart - * @param array $chartSettings settings used to generate the chart - * @return string HTML and JS code for the chart - */ -function PMA_chart_results($data, &$chartSettings) -{ - $chartData = array(); - $chart = null; - - // set default title if not already set - if (empty($chartSettings['titleText'])) { - $chartSettings['titleText'] = __('Query results'); - } - - // set default type if not already set - if (empty($chartSettings['type'])) { - $chartSettings['type'] = 'bar'; - } - - // set default type if not already set - if (empty($chartSettings['continuous'])) { - $chartSettings['continuous'] = 'off'; - } - - // set default bar type if needed - if ($chartSettings['type'] == 'bar' && empty($chartSettings['barType'])) { - $chartSettings['barType'] = 'stacked'; - } - - // default for legend - $chartSettings['legend'] = false; - - // default for muti series - $chartSettings['multi'] = false; - - if (! isset($data[0])) { - // empty data - return __('No data found for the chart.'); - } - - if (count($data[0]) == 1 || count($data[0]) == 2) { - // One or two columns in every row. - // This data is suitable for a simple bar chart. - - if ($chartSettings['type'] == 'pie') { - // loop through the rows, data for pie chart has to be formated - // in a different way then in other charts. - foreach ($data as $rowKey => $row) { - $values = array_values($row); - - if (count($row) == 1) { - $chartData[$rowKey] = $values[0]; - } - else { - $chartData[$values[1]] = $values[0]; - } - } - - $chartSettings['legend'] = true; - $chart = new PMA_pChart_pie($chartData, $chartSettings); - } - else { - // loop through the rows - foreach ($data as $rowKey => $row) { - - // loop through the columns in the row - foreach ($row as $valueKey => $value) { - $chartData[$valueKey][] = $value; - } - - // if only one column, we need to add - // placeholder data for x axis - if (count($row) == 1) { - $chartData[''][] = $rowKey; - } - } - - switch ($chartSettings['type']) { - case 'bar': - default: - $chart = new PMA_pChart_single_bar($chartData, $chartSettings); - break; - case 'line': - $chart = new PMA_pChart_single_line($chartData, $chartSettings); - break; - case 'radar': - $chart = new PMA_pChart_single_radar($chartData, $chartSettings); - break; - } - } - } - else if (count($data[0]) == 3) { - // Three columns (x axis, y axis, series) in every row. - // This data is suitable for a stacked bar chart. - $chartSettings['multi'] = true; - - $keys = array_keys($data[0]); - $yAxisKey = $keys[0]; - $xAxisKey = $keys[1]; - $seriesKey = $keys[2]; - - // get all the series labels - $seriesLabels = array(); - foreach ($data as $row) { - $seriesLabels[] = $row[$seriesKey]; - } - $seriesLabels = array_unique($seriesLabels); - - // loop through the rows - $currentXLabel = $data[0][$xAxisKey]; - foreach ($data as $row) { - - // save the label - // use the same value as the key and the value to get rid of duplicate results - $chartData[$xAxisKey][$row[$xAxisKey]] = $row[$xAxisKey]; - - // make sure to set value to every serie - $currentSeriesLabel = (string)$row[$seriesKey]; - foreach ($seriesLabels as $seriesLabelsValue) { - if ($currentSeriesLabel == $seriesLabelsValue) { - // the value os for this serie - $chartData[$yAxisKey][$seriesLabelsValue][$row[$xAxisKey]] = (int)$row[$yAxisKey]; - } - else if (! isset($chartData[$yAxisKey][$seriesLabelsValue][$row[$xAxisKey]])) { - // if the value for this serie is not set, set it to 0 - $chartData[$yAxisKey][$seriesLabelsValue][$row[$xAxisKey]] = 0; - } - } - } - - $chartSettings['legend'] = true; - - // determine the chart type - switch ($chartSettings['type']) { - case 'bar': - default: - - // determine the bar chart type - switch ($chartSettings['barType']) { - case 'stacked': - default: - $chart = new PMA_pChart_stacked_bar($chartData, $chartSettings); - break; - case 'multi': - $chart = new PMA_pChart_multi_bar($chartData, $chartSettings); - break; - } - break; - - case 'line': - $chart = new PMA_pChart_multi_line($chartData, $chartSettings); - break; - case 'radar': - $chart = new PMA_pChart_multi_radar($chartData, $chartSettings); - break; - } - } - else { - // unknown data format - return ''; - } - - $chartCode = $chart->toString(); - $chartSettings = $chart->getSettings(); - $chartErrors = $chart->getErrors(); - PMA_handle_chart_err($chartErrors); - - return $chartCode; -} - -/** - * Simple handler of chart errors. - * @param array $errors all occured errors - */ -function PMA_handle_chart_err($errors) -{ - if (in_array(ERR_NO_GD, $errors)) { - PMA_warnMissingExtension('GD', false, __('GD extension is needed for charts.')); - } - else if (in_array(ERR_NO_JSON, $errors)) { - PMA_warnMissingExtension('JSON', false, __('JSON encoder is needed for chart tooltips.')); - } -} - -?> diff --git a/libraries/chart/pChart/fonts/DejaVuSans.ttf b/libraries/chart/pChart/fonts/DejaVuSans.ttf deleted file mode 100644 index a99969e..0000000 Binary files a/libraries/chart/pChart/fonts/DejaVuSans.ttf and /dev/null differ diff --git a/libraries/chart/pChart/fonts/LICENSE b/libraries/chart/pChart/fonts/LICENSE deleted file mode 100644 index 254e2cc..0000000 --- a/libraries/chart/pChart/fonts/LICENSE +++ /dev/null @@ -1,99 +0,0 @@ -Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. -Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) - -Bitstream Vera Fonts Copyright ------------------------------- - -Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is -a trademark of Bitstream, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of the fonts accompanying this license ("Fonts") and associated -documentation files (the "Font Software"), to reproduce and distribute the -Font Software, including without limitation the rights to use, copy, merge, -publish, distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to the -following conditions: - -The above copyright and trademark notices and this permission notice shall -be included in all copies of one or more of the Font Software typefaces. - -The Font Software may be modified, altered, or added to, and in particular -the designs of glyphs or characters in the Fonts may be modified and -additional glyphs or characters may be added to the Fonts, only if the fonts -are renamed to names not containing either the words "Bitstream" or the word -"Vera". - -This License becomes null and void to the extent applicable to Fonts or Font -Software that has been modified and is distributed under the "Bitstream -Vera" names. - -The Font Software may be sold as part of a larger software package but no -copy of one or more of the Font Software typefaces may be sold by itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, -TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME -FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING -ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE -FONT SOFTWARE. - -Except as contained in this notice, the names of Gnome, the Gnome -Foundation, and Bitstream Inc., shall not be used in advertising or -otherwise to promote the sale, use or other dealings in this Font Software -without prior written authorization from the Gnome Foundation or Bitstream -Inc., respectively. For further information, contact: fonts at gnome dot -org. - -Arev Fonts Copyright ------------------------------- - -Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of the fonts accompanying this license ("Fonts") and -associated documentation files (the "Font Software"), to reproduce -and distribute the modifications to the Bitstream Vera Font Software, -including without limitation the rights to use, copy, merge, publish, -distribute, and/or sell copies of the Font Software, and to permit -persons to whom the Font Software is furnished to do so, subject to -the following conditions: - -The above copyright and trademark notices and this permission notice -shall be included in all copies of one or more of the Font Software -typefaces. - -The Font Software may be modified, altered, or added to, and in -particular the designs of glyphs or characters in the Fonts may be -modified and additional glyphs or characters may be added to the -Fonts, only if the fonts are renamed to names not containing either -the words "Tavmjong Bah" or the word "Arev". - -This License becomes null and void to the extent applicable to Fonts -or Font Software that has been modified and is distributed under the -"Tavmjong Bah Arev" names. - -The Font Software may be sold as part of a larger software package but -no copy of one or more of the Font Software typefaces may be sold by -itself. - -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL -TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. - -Except as contained in this notice, the name of Tavmjong Bah shall not -be used in advertising or otherwise to promote the sale, use or other -dealings in this Font Software without prior written authorization -from Tavmjong Bah. For further information, contact: tavmjong @ free -. fr. - -$Id: LICENSE 2133 2007-11-28 02:46:28Z lechimp $ diff --git a/libraries/chart/pChart/fonts/README b/libraries/chart/pChart/fonts/README deleted file mode 100644 index 554d017..0000000 --- a/libraries/chart/pChart/fonts/README +++ /dev/null @@ -1,6 +0,0 @@ - -The tahoma.ttf file which is bundled with pChart 1.27d was replaced due to uncertainty with it's licence. - -As a replacement the DejaVuSans.ttf font file was extracted from the DejaVu fonts 2.32 package available from dejavu-fonts.org - -For license information see LICENSE. diff --git a/libraries/chart/pChart/pCache.class b/libraries/chart/pChart/pCache.class deleted file mode 100644 index 2bcd6b0..0000000 --- a/libraries/chart/pChart/pCache.class +++ /dev/null @@ -1,119 +0,0 @@ -<?php - /* - pCache - Faster renderding using data cache - Copyright (C) 2008 Jean-Damien POGOLOTTI - Version 1.1.2 last updated on 06/17/08 - - http://pchart.sourceforge.net - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 1,2,3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - - Class initialisation : - pCache($CacheFolder="Cache/") - Cache management : - IsInCache($Data) - GetFromCache($ID,$Data) - WriteToCache($ID,$Data,$Picture) - DeleteFromCache($ID,$Data) - ClearCache() - Inner functions : - GetHash($ID,$Data) - */ - - /* pCache class definition */ - class pCache - { - var $HashKey = ""; - var $CacheFolder = "Cache/"; - - /* Create the pCache object */ - function pCache($CacheFolder="Cache/") - { - $this->CacheFolder = $CacheFolder; - } - - /* This function is clearing the cache folder */ - function ClearCache() - { - if ($handle = opendir($this->CacheFolder)) - { - while (false !== ($file = readdir($handle))) - { - if ( $file != "." && $file != ".." ) - unlink($this->CacheFolder.$file); - } - closedir($handle); - } - } - - /* This function is checking if we have an offline version of this chart */ - function IsInCache($ID,$Data,$Hash="") - { - if ( $Hash == "" ) - $Hash = $this->GetHash($ID,$Data); - - if ( file_exists($this->CacheFolder.$Hash) ) - return(TRUE); - else - return(FALSE); - } - - /* This function is making a copy of drawn chart in the cache folder */ - function WriteToCache($ID,$Data,$Picture) - { - $Hash = $this->GetHash($ID,$Data); - $FileName = $this->CacheFolder.$Hash; - - imagepng($Picture->Picture,$FileName); - } - - /* This function is removing any cached copy of this chart */ - function DeleteFromCache($ID,$Data) - { - $Hash = $this->GetHash($ID,$Data); - $FileName = $this->CacheFolder.$Hash; - - if ( file_exists($FileName ) ) - unlink($FileName); - } - - /* This function is retrieving the cached picture if applicable */ - function GetFromCache($ID,$Data) - { - $Hash = $this->GetHash($ID,$Data); - if ( $this->IsInCache("","",$Hash ) ) - { - $FileName = $this->CacheFolder.$Hash; - - header('Content-type: image/png'); - @readfile($FileName); - exit(); - } - } - - /* This function is building the graph unique hash key */ - function GetHash($ID,$Data) - { - $mKey = "$ID"; - foreach($Data as $key => $Values) - { - $tKey = ""; - foreach($Values as $Serie => $Value) - $tKey = $tKey.$Serie.$Value; - $mKey = $mKey.md5($tKey); - } - return(md5($mKey)); - } - } -?> \ No newline at end of file diff --git a/libraries/chart/pChart/pChart.class b/libraries/chart/pChart/pChart.class deleted file mode 100644 index 2b5a077..0000000 --- a/libraries/chart/pChart/pChart.class +++ /dev/null @@ -1,3626 +0,0 @@ -<?php - /* - pChart - a PHP class to build charts! - Copyright (C) 2008 Jean-Damien POGOLOTTI - Version 1.27d last updated on 09/30/08 - - http://pchart.sourceforge.net - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 1,2,3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - - Class initialisation : - pChart($XSize,$YSize) - Draw methods : - drawBackground($R,$G,$B) - drawRectangle($X1,$Y1,$X2,$Y2,$R,$G,$B) - drawFilledRectangle($X1,$Y1,$X2,$Y2,$R,$G,$B,$DrawBorder=TRUE,$Alpha=100) - drawRoundedRectangle($X1,$Y1,$X2,$Y2,$Radius,$R,$G,$B) - drawFilledRoundedRectangle($X1,$Y1,$X2,$Y2,$Radius,$R,$G,$B) - drawCircle($Xc,$Yc,$Height,$R,$G,$B,$Width=0) - drawFilledCircle($Xc,$Yc,$Height,$R,$G,$B,$Width=0) - drawEllipse($Xc,$Yc,$Height,$Width,$R,$G,$B) - drawFilledEllipse($Xc,$Yc,$Height,$Width,$R,$G,$B) - drawLine($X1,$Y1,$X2,$Y2,$R,$G,$B,$GraphFunction=FALSE) - drawDottedLine($X1,$Y1,$X2,$Y2,$DotSize,$R,$G,$B) - drawAlphaPixel($X,$Y,$Alpha,$R,$G,$B) - drawFromPNG($FileName,$X,$Y,$Alpha=100) - drawFromGIF($FileName,$X,$Y,$Alpha=100) - drawFromJPG($FileName,$X,$Y,$Alpha=100) - Graph setup methods : - addBorder($Width=3,$R=0,$G=0,$B=0) - clearScale() - clearShadow() - createColorGradientPalette($R1,$G1,$B1,$R2,$G2,$B2,$Shades) - drawGraphArea($R,$G,$B,$Stripe=FALSE) - drawScale($Data,$DataDescription,$ScaleMode,$R,$G,$B,$DrawTicks=TRUE,$Angle=0,$Decimals=1,$WithMargin=FALSE,$SkipLabels=1,$RightScale=FALSE) - drawRightScale($Data,$DataDescription,$ScaleMode,$R,$G,$B,$DrawTicks=TRUE,$Angle=0,$Decimals=1,$WithMargin=FALSE,$SkipLabels=1) - drawXYScale($Data,$DataDescription,$YSerieName,$XSerieName,$R,$G,$B,$WithMargin=0,$Angle=0,$Decimals=1) - drawGrid($LineWidth,$Mosaic=TRUE,$R=220,$G=220,$B=220,$Alpha=100) - drawLegend($XPos,$YPos,$DataDescription,$R,$G,$B,$Rs=-1,$Gs=-1,$Bs=-1,$Rt=0,$Gt=0,$Bt=0,$Border=FALSE) - drawPieLegend($XPos,$YPos,$Data,$DataDescription,$R,$G,$B) - drawTitle($XPos,$YPos,$Value,$R,$G,$B,$XPos2=-1,$YPos2=-1,$Shadow=FALSE) - drawTreshold($Value,$R,$G,$B,$ShowLabel=FALSE,$ShowOnRight=FALSE,$TickWidth=4,$FreeText=NULL) - drawArea($Data,$Serie1,$Serie2,$R,$G,$B,$Alpha = 50) - drawRadarAxis($Data,$DataDescription,$Mosaic=TRUE,$BorderOffset=10,$A_R=60,$A_G=60,$A_B=60,$S_R=200,$S_G=200,$S_B=200,$MaxValue=-1) - drawGraphAreaGradient($R,$G,$B,$Decay,$Target=TARGET_GRAPHAREA) - drawTextBox($X1,$Y1,$X2,$Y2,$Text,$Angle=0,$R=255,$G=255,$B=255,$Align=ALIGN_LEFT,$Shadow=TRUE,$BgR=-1,$BgG=-1,$BgB=-1,$Alpha=100) - getLegendBoxSize($DataDescription) - loadColorPalette($FileName,$Delimiter=",") - reportWarnings($Interface="CLI") - setGraphArea($X1,$Y1,$X2,$Y2) - setLabel($Data,$DataDescription,$SerieName,$ValueName,$Caption,$R=210,$G=210,$B=210) - setColorPalette($ID,$R,$G,$B) - setCurrency($Currency) - setDateFormat($Format) - setFontProperties($FontName,$FontSize) - setLineStyle($Width=1,$DotSize=0) - setFixedScale($VMin,$VMax,$Divisions=5,$VXMin=0,$VXMin=0,$XDivisions=5) - setShadowProperties($XDistance=1,$YDistance=1,$R=60,$G=60,$B=60,$Alpha) - writeValues($Data,$DataDescription,$Series) - Graphs methods : - drawPlotGraph($Data,$DataDescription,$BigRadius=5,$SmallRadius=2,$R2=-1,$G2=-1,$B2=-1,$Shadow=FALSE) - drawXYPlotGraph($Data,$DataDescription,$YSerieName,$XSerieName,$PaletteID=0,$BigRadius=5,$SmallRadius=2,$R2=-1,$G2=-1,$B2=-1) - drawLineGraph($Data,$DataDescription,$SerieName="") - drawXYGraph($Data,$DataDescription,$YSerieName,$XSerieName,$PaletteID=0) - drawFilledLineGraph($Data,$DataDescription,$Alpha=100,$AroundZero=FALSE) - drawCubicCurve($Data,$DataDescription,$Accuracy=.1,$SerieName="") - drawFilledCubicCurve($Data,$DataDescription,$Accuracy=.1,$Alpha=100,$AroundZero=FALSE) - drawOverlayBarGraph($Data,$DataDescription,$Alpha=50) - drawBarGraph($Data,$DataDescription,$Shadow=FALSE) - drawStackedBarGraph($Data,$DataDescription,$Alpha=50,$Contiguous=FALSE) - drawLimitsGraph($Data,$DataDescription,$R=0,$G=0,$B=0) - drawRadar($Data,$DataDescription,$BorderOffset=10,$MaxValue=-1) - drawFilledRadar($Data,$DataDescription,$Alpha=50,$BorderOffset=10,$MaxValue=-1) - drawBasicPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$R=255,$G=255,$B=255,$Decimals=0) - drawFlatPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$SpliceDistance=0,$Decimals = 0) - drawFlatPieGraphWithShadow($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$SpliceDistance=0,$Decimals = 0) - drawPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$EnhanceColors=TRUE,$Skew=60,$SpliceHeight=20,$SpliceDistance=0,$Decimals=0) - Other methods : - setImageMap($Mode=TRUE,$GraphID="MyGraph") - getImageMap() - getSavedImageMap($MapName,$Flush=TRUE) - Render($FileName) - Stroke() - */ - - /* Declare some script wide constants */ - define("SCALE_NORMAL",1); - define("SCALE_ADDALL",2); - define("SCALE_START0",3); - define("SCALE_ADDALLSTART0",4); - define("PIE_PERCENTAGE", 1); - define("PIE_LABELS",2); - define("PIE_NOLABEL",3); - define("PIE_PERCENTAGE_LABEL", 4); - define("TARGET_GRAPHAREA",1); - define("TARGET_BACKGROUND",2); - define("ALIGN_TOP_LEFT",1); - define("ALIGN_TOP_CENTER",2); - define("ALIGN_TOP_RIGHT",3); - define("ALIGN_LEFT",4); - define("ALIGN_CENTER",5); - define("ALIGN_RIGHT",6); - define("ALIGN_BOTTOM_LEFT",7); - define("ALIGN_BOTTOM_CENTER",8); - define("ALIGN_BOTTOM_RIGHT",9); - - /* pChart class definition */ - class pChart - { - /* Palettes definition */ - var $Palette = array("0"=>array("R"=>188,"G"=>224,"B"=>46), - "1"=>array("R"=>224,"G"=>100,"B"=>46), - "2"=>array("R"=>224,"G"=>214,"B"=>46), - "3"=>array("R"=>46,"G"=>151,"B"=>224), - "4"=>array("R"=>176,"G"=>46,"B"=>224), - "5"=>array("R"=>224,"G"=>46,"B"=>117), - "6"=>array("R"=>92,"G"=>224,"B"=>46), - "7"=>array("R"=>224,"G"=>176,"B"=>46)); - - /* Some static vars used in the class */ - var $XSize = NULL; - var $YSize = NULL; - var $Picture = NULL; - var $ImageMap = NULL; - - /* Error management */ - var $ErrorReporting = FALSE; - var $ErrorInterface = "CLI"; - var $Errors = NULL; - var $ErrorFontName = "Fonts/pf_arma_five.ttf"; - var $ErrorFontSize = 6; - - /* vars related to the graphing area */ - var $GArea_X1 = NULL; - var $GArea_Y1 = NULL; - var $GArea_X2 = NULL; - var $GArea_Y2 = NULL; - var $GAreaXOffset = NULL; - var $VMax = NULL; - var $VMin = NULL; - var $VXMax = NULL; - var $VXMin = NULL; - var $Divisions = NULL; - var $XDivisions = NULL; - var $DivisionHeight = NULL; - var $XDivisionHeight = NULL; - var $DivisionCount = NULL; - var $XDivisionCount = NULL; - var $DivisionRatio = NULL; - var $XDivisionRatio = NULL; - var $DivisionWidth = NULL; - var $DataCount = NULL; - var $Currency = "\$"; - - /* Text format related vars */ - var $FontName = NULL; - var $FontSize = NULL; - var $DateFormat = "d/m/Y"; - - /* Lines format related vars */ - var $LineWidth = 1; - var $LineDotSize = 0; - - /* Layer related vars */ - var $Layers = NULL; - - /* Set antialias quality : 0 is maximum, 100 minimum*/ - var $AntialiasQuality = 0; - - /* Shadow settings */ - var $ShadowActive = FALSE; - var $ShadowXDistance = 1; - var $ShadowYDistance = 1; - var $ShadowRColor = 60; - var $ShadowGColor = 60; - var $ShadowBColor = 60; - var $ShadowAlpha = 50; - var $ShadowBlur = 0; - - /* Image Map settings */ - var $BuildMap = FALSE; - var $MapFunction = NULL; - var $tmpFolder = "tmp/"; - var $MapID = NULL; - - /* This function create the background picture */ - function pChart($XSize,$YSize) - { - $this->XSize = $XSize; - $this->YSize = $YSize; - $this->Picture = imagecreatetruecolor($XSize,$YSize); - $C_White =$this->AllocateColor($this->Picture,255,255,255); - imagefilledrectangle($this->Picture,0,0,$XSize,$YSize,$C_White); - imagecolortransparent($this->Picture,$C_White); - $this->setFontProperties("tahoma.ttf",8); - } - - /* Set if warnings should be reported */ - function reportWarnings($Interface="CLI") - { - $this->ErrorReporting = TRUE; - $this->ErrorInterface = $Interface; - } - - /* Set the font properties */ - function setFontProperties($FontName,$FontSize) - { - $this->FontName = $FontName; - $this->FontSize = $FontSize; - } - - /* Set the shadow properties */ - function setShadowProperties($XDistance=1,$YDistance=1,$R=60,$G=60,$B=60,$Alpha=50,$Blur=0) - { - $this->ShadowActive = TRUE; - $this->ShadowXDistance = $XDistance; - $this->ShadowYDistance = $YDistance; - $this->ShadowRColor = $R; - $this->ShadowGColor = $G; - $this->ShadowBColor = $B; - $this->ShadowAlpha = $Alpha; - $this->ShadowBlur = $Blur; - } - - /* Remove shadow option */ - function clearShadow() - { - $this->ShadowActive = FALSE; - } - - /* Set Palette color */ - function setColorPalette($ID,$R,$G,$B) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $this->Palette[$ID]["R"] = $R; - $this->Palette[$ID]["G"] = $G; - $this->Palette[$ID]["B"] = $B; - } - - /* Create a color palette shading from one color to another */ - function createColorGradientPalette($R1,$G1,$B1,$R2,$G2,$B2,$Shades) - { - $RFactor = ($R2-$R1)/$Shades; - $GFactor = ($G2-$G1)/$Shades; - $BFactor = ($B2-$B1)/$Shades; - - for($i=0;$i<=$Shades-1;$i++) - { - $this->Palette[$i]["R"] = $R1+$RFactor*$i; - $this->Palette[$i]["G"] = $G1+$GFactor*$i; - $this->Palette[$i]["B"] = $B1+$BFactor*$i; - } - } - - /* Load Color Palette from file */ - function loadColorPalette($FileName,$Delimiter=",") - { - $handle = @fopen($FileName,"r"); - $ColorID = 0; - if ($handle) - { - while (!feof($handle)) - { - $buffer = fgets($handle, 4096); - $buffer = str_replace(chr(10),"",$buffer); - $buffer = str_replace(chr(13),"",$buffer); - $Values = split($Delimiter,$buffer); - if ( count($Values) == 3 ) - { - $this->Palette[$ColorID]["R"] = $Values[0]; - $this->Palette[$ColorID]["G"] = $Values[1]; - $this->Palette[$ColorID]["B"] = $Values[2]; - $ColorID++; - } - } - } - } - - /* Set line style */ - function setLineStyle($Width=1,$DotSize=0) - { - $this->LineWidth = $Width; - $this->LineDotSize = $DotSize; - } - - /* Set currency symbol */ - function setCurrency($Currency) - { - $this->Currency = $Currency; - } - - /* Set the graph area location */ - function setGraphArea($X1,$Y1,$X2,$Y2) - { - $this->GArea_X1 = $X1; - $this->GArea_Y1 = $Y1; - $this->GArea_X2 = $X2; - $this->GArea_Y2 = $Y2; - } - - /* Prepare the graph area */ - function drawGraphArea($R,$G,$B,$Stripe=FALSE) - { - $this->drawFilledRectangle($this->GArea_X1,$this->GArea_Y1,$this->GArea_X2,$this->GArea_Y2,$R,$G,$B,FALSE); - $this->drawRectangle($this->GArea_X1,$this->GArea_Y1,$this->GArea_X2,$this->GArea_Y2,$R-40,$G-40,$B-40); - - if ( $Stripe ) - { - $R2 = $R-15; if ( $R2 < 0 ) { $R2 = 0; } - $G2 = $R-15; if ( $G2 < 0 ) { $G2 = 0; } - $B2 = $R-15; if ( $B2 < 0 ) { $B2 = 0; } - - $LineColor =$this->AllocateColor($this->Picture,$R2,$G2,$B2); - $SkewWidth = $this->GArea_Y2-$this->GArea_Y1-1; - - for($i=$this->GArea_X1-$SkewWidth;$i<=$this->GArea_X2;$i=$i+4) - { - $X1 = $i; $Y1 = $this->GArea_Y2; - $X2 = $i+$SkewWidth; $Y2 = $this->GArea_Y1; - - - if ( $X1 < $this->GArea_X1 ) - { $X1 = $this->GArea_X1; $Y1 = $this->GArea_Y1 + $X2 - $this->GArea_X1 + 1; } - - if ( $X2 >= $this->GArea_X2 ) - { $Y2 = $this->GArea_Y1 + $X2 - $this->GArea_X2 +1; $X2 = $this->GArea_X2 - 1; } -// * Fixed in 1.27 * { $X2 = $this->GArea_X2 - 1; $Y2 = $this->GArea_Y2 - ($this->GArea_X2 - $X1); } - - imageline($this->Picture,$X1,$Y1,$X2,$Y2+1,$LineColor); - } - } - } - - /* Allow you to clear the scale : used if drawing multiple charts */ - function clearScale() - { - $this->VMin = NULL; - $this->VMax = NULL; - $this->VXMin = NULL; - $this->VXMax = NULL; - $this->Divisions = NULL; - $this->XDivisions = NULL; } - - /* Allow you to fix the scale, use this to bypass the automatic scaling */ - function setFixedScale($VMin,$VMax,$Divisions=5,$VXMin=0,$VXMax=0,$XDivisions=5) - { - $this->VMin = $VMin; - $this->VMax = $VMax; - $this->Divisions = $Divisions; - - if ( !$VXMin == 0 ) - { - $this->VXMin = $VXMin; - $this->VXMax = $VXMax; - $this->XDivisions = $XDivisions; - } - } - - /* Wrapper to the drawScale() function allowing a second scale to be drawn */ - function drawRightScale($Data,$DataDescription,$ScaleMode,$R,$G,$B,$DrawTicks=TRUE,$Angle=0,$Decimals=1,$WithMargin=FALSE,$SkipLabels=1) - { - $this->drawScale($Data,$DataDescription,$ScaleMode,$R,$G,$B,$DrawTicks,$Angle,$Decimals,$WithMargin,$SkipLabels,TRUE); - } - - /* Compute and draw the scale */ - function drawScale($Data,$DataDescription,$ScaleMode,$R,$G,$B,$DrawTicks=TRUE,$Angle=0,$Decimals=1,$WithMargin=FALSE,$SkipLabels=1,$RightScale=FALSE) - { - /* Validate the Data and DataDescription array */ - $this->validateData("drawScale",$Data); - - $C_TextColor =$this->AllocateColor($this->Picture,$R,$G,$B); - - $this->drawLine($this->GArea_X1,$this->GArea_Y1,$this->GArea_X1,$this->GArea_Y2,$R,$G,$B); - $this->drawLine($this->GArea_X1,$this->GArea_Y2,$this->GArea_X2,$this->GArea_Y2,$R,$G,$B); - - if ( $this->VMin == NULL && $this->VMax == NULL) - { - if (isset($DataDescription["Values"][0])) - { - $this->VMin = $Data[0][$DataDescription["Values"][0]]; - $this->VMax = $Data[0][$DataDescription["Values"][0]]; - } - else { $this->VMin = 2147483647; $this->VMax = -2147483647; } - - /* Compute Min and Max values */ - if ( $ScaleMode == SCALE_NORMAL || $ScaleMode == SCALE_START0 ) - { - if ( $ScaleMode == SCALE_START0 ) { $this->VMin = 0; } - - foreach ( $Data as $Key => $Values ) - { - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if (isset($Data[$Key][$ColName])) - { - $Value = $Data[$Key][$ColName]; - - if ( is_numeric($Value) ) - { - if ( $this->VMax < $Value) { $this->VMax = $Value; } - if ( $this->VMin > $Value) { $this->VMin = $Value; } - } - } - } - } - } - elseif ( $ScaleMode == SCALE_ADDALL || $ScaleMode == SCALE_ADDALLSTART0 ) /* Experimental */ - { - if ( $ScaleMode == SCALE_ADDALLSTART0 ) { $this->VMin = 0; } - - foreach ( $Data as $Key => $Values ) - { - $Sum = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if (isset($Data[$Key][$ColName])) - { - $Value = $Data[$Key][$ColName]; - if ( is_numeric($Value) ) - $Sum += $Value; - } - } - if ( $this->VMax < $Sum) { $this->VMax = $Sum; } - if ( $this->VMin > $Sum) { $this->VMin = $Sum; } - } - } - - if ( $this->VMax > preg_replace('/\.[0-9]+/','',$this->VMax) ) - $this->VMax = preg_replace('/\.[0-9]+/','',$this->VMax)+1; - - /* If all values are the same */ - if ( $this->VMax == $this->VMin ) - { - if ( $this->VMax >= 0 ) { $this->VMax++; } - else { $this->VMin--; } - } - - $DataRange = $this->VMax - $this->VMin; - if ( $DataRange == 0 ) { $DataRange = .1; } - - /* Compute automatic scaling */ - $ScaleOk = FALSE; $Factor = 1; - $MinDivHeight = 25; $MaxDivs = ($this->GArea_Y2 - $this->GArea_Y1) / $MinDivHeight; - - if ( $this->VMin == 0 && $this->VMax == 0 ) - { $this->VMin = 0; $this->VMax = 2; $Scale = 1; $Divisions = 2;} - elseif ($MaxDivs > 1) - { - while(!$ScaleOk) - { - $Scale1 = ( $this->VMax - $this->VMin ) / $Factor; - $Scale2 = ( $this->VMax - $this->VMin ) / $Factor / 2; - $Scale4 = ( $this->VMax - $this->VMin ) / $Factor / 4; - - if ( $Scale1 > 1 && $Scale1 <= $MaxDivs && !$ScaleOk) { $ScaleOk = TRUE; $Divisions = floor($Scale1); $Scale = 1;} - if ( $Scale2 > 1 && $Scale2 <= $MaxDivs && !$ScaleOk) { $ScaleOk = TRUE; $Divisions = floor($Scale2); $Scale = 2;} - if (!$ScaleOk) - { - if ( $Scale2 > 1 ) { $Factor = $Factor * 10; } - if ( $Scale2 < 1 ) { $Factor = $Factor / 10; } - } - } - - if ( floor($this->VMax / $Scale / $Factor) != $this->VMax / $Scale / $Factor) - { - $GridID = floor ( $this->VMax / $Scale / $Factor) + 1; - $this->VMax = $GridID * $Scale * $Factor; - $Divisions++; - } - - if ( floor($this->VMin / $Scale / $Factor) != $this->VMin / $Scale / $Factor) - { - $GridID = floor( $this->VMin / $Scale / $Factor); - $this->VMin = $GridID * $Scale * $Factor; - $Divisions++; - } - } - else /* Can occurs for small graphs */ - $Scale = 1; - - if ( !isset($Divisions) ) - $Divisions = 2; - - if ($Scale == 1 && $Divisions%2 == 1) - $Divisions--; - } - else - $Divisions = $this->Divisions; - - $this->DivisionCount = $Divisions; - - $DataRange = $this->VMax - $this->VMin; - if ( $DataRange == 0 ) { $DataRange = .1; } - - $this->DivisionHeight = ( $this->GArea_Y2 - $this->GArea_Y1 ) / $Divisions; - $this->DivisionRatio = ( $this->GArea_Y2 - $this->GArea_Y1 ) / $DataRange; - - $this->GAreaXOffset = 0; - if ( count($Data) > 1 ) - { - if ( $WithMargin == FALSE ) - $this->DivisionWidth = ( $this->GArea_X2 - $this->GArea_X1 ) / (count($Data)-1); - else - { - $this->DivisionWidth = ( $this->GArea_X2 - $this->GArea_X1 ) / (count($Data)); - $this->GAreaXOffset = $this->DivisionWidth / 2; - } - } - else - { - $this->DivisionWidth = $this->GArea_X2 - $this->GArea_X1; - $this->GAreaXOffset = $this->DivisionWidth / 2; - } - - $this->DataCount = count($Data); - - if ( $DrawTicks == FALSE ) - return(0); - - $YPos = $this->GArea_Y2; $XMin = NULL; - for($i=1;$i<=$Divisions+1;$i++) - { - if ( $RightScale ) - $this->drawLine($this->GArea_X2,$YPos,$this->GArea_X2+5,$YPos,$R,$G,$B); - else - $this->drawLine($this->GArea_X1,$YPos,$this->GArea_X1-5,$YPos,$R,$G,$B); - - $Value = $this->VMin + ($i-1) * (( $this->VMax - $this->VMin ) / $Divisions); - $Value = round($Value * pow(10,$Decimals)) / pow(10,$Decimals); - if ( $DataDescription["Format"]["Y"] == "number" ) - $Value = $Value.$DataDescription["Unit"]["Y"]; - if ( $DataDescription["Format"]["Y"] == "time" ) - $Value = $this->ToTime($Value); - if ( $DataDescription["Format"]["Y"] == "date" ) - $Value = $this->ToDate($Value); - if ( $DataDescription["Format"]["Y"] == "metric" ) - $Value = $this->ToMetric($Value); - if ( $DataDescription["Format"]["Y"] == "currency" ) - $Value = $this->ToCurrency($Value); - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextWidth = $Position[2]-$Position[0]; - - if ( $RightScale ) - { - imagettftext($this->Picture,$this->FontSize,0,$this->GArea_X2+10,$YPos+($this->FontSize/2),$C_TextColor,$this->FontName,$Value); - if ( $XMin < $this->GArea_X2+15+$TextWidth || $XMin == NULL ) { $XMin = $this->GArea_X2+15+$TextWidth; } - } - else - { - imagettftext($this->Picture,$this->FontSize,0,$this->GArea_X1-10-$TextWidth,$YPos+($this->FontSize/2),$C_TextColor,$this->FontName,$Value); - if ( $XMin > $this->GArea_X1-10-$TextWidth || $XMin == NULL ) { $XMin = $this->GArea_X1-10-$TextWidth; } - } - - $YPos = $YPos - $this->DivisionHeight; - } - - /* Write the Y Axis caption if set */ - if ( isset($DataDescription["Axis"]["Y"]) ) - { - $Position = imageftbbox($this->FontSize,90,$this->FontName,$DataDescription["Axis"]["Y"]); - $TextHeight = abs($Position[1])+abs($Position[3]); - $TextTop = (($this->GArea_Y2 - $this->GArea_Y1) / 2) + $this->GArea_Y1 + ($TextHeight/2); - - if ( $RightScale ) - imagettftext($this->Picture,$this->FontSize,90,$XMin+$this->FontSize,$TextTop,$C_TextColor,$this->FontName,$DataDescription["Axis"]["Y"]); - else - imagettftext($this->Picture,$this->FontSize,90,$XMin-$this->FontSize,$TextTop,$C_TextColor,$this->FontName,$DataDescription["Axis"]["Y"]); - } - - /* Horizontal Axis */ - $XPos = $this->GArea_X1 + $this->GAreaXOffset; - $ID = 1; $YMax = NULL; - foreach ( $Data as $Key => $Values ) - { - if ( $ID % $SkipLabels == 0 ) - { - $this->drawLine(floor($XPos),$this->GArea_Y2,floor($XPos),$this->GArea_Y2+5,$R,$G,$B); - $Value = $Data[$Key][$DataDescription["Position"]]; - if ( $DataDescription["Format"]["X"] == "number" ) - $Value = $Value.$DataDescription["Unit"]["X"]; - if ( $DataDescription["Format"]["X"] == "time" ) - $Value = $this->ToTime($Value); - if ( $DataDescription["Format"]["X"] == "date" ) - $Value = $this->ToDate($Value); - if ( $DataDescription["Format"]["X"] == "metric" ) - $Value = $this->ToMetric($Value); - if ( $DataDescription["Format"]["X"] == "currency" ) - $Value = $this->ToCurrency($Value); - - $Position = imageftbbox($this->FontSize,$Angle,$this->FontName,$Value); - $TextWidth = abs($Position[2])+abs($Position[0]); - $TextHeight = abs($Position[1])+abs($Position[3]); - - if ( $Angle == 0 ) - { - $YPos = $this->GArea_Y2+18; - imagettftext($this->Picture,$this->FontSize,$Angle,floor($XPos)-floor($TextWidth/2),$YPos,$C_TextColor,$this->FontName,$Value); - } - else - { - $YPos = $this->GArea_Y2+10+$TextHeight; - if ( $Angle <= 90 ) - imagettftext($this->Picture,$this->FontSize,$Angle,floor($XPos)-$TextWidth+5,$YPos,$C_TextColor,$this->FontName,$Value); - else - imagettftext($this->Picture,$this->FontSize,$Angle,floor($XPos)+$TextWidth+5,$YPos,$C_TextColor,$this->FontName,$Value); - } - if ( $YMax < $YPos || $YMax == NULL ) { $YMax = $YPos; } - } - - $XPos = $XPos + $this->DivisionWidth; - $ID++; - } - - /* Write the X Axis caption if set */ - if ( isset($DataDescription["Axis"]["X"]) ) - { - $Position = imageftbbox($this->FontSize,90,$this->FontName,$DataDescription["Axis"]["X"]); - $TextWidth = abs($Position[2])+abs($Position[0]); - $TextLeft = (($this->GArea_X2 - $this->GArea_X1) / 2) + $this->GArea_X1 + ($TextWidth/2); - imagettftext($this->Picture,$this->FontSize,0,$TextLeft,$YMax+$this->FontSize+5,$C_TextColor,$this->FontName,$DataDescription["Axis"]["X"]); - } - } - - /* Compute and draw the scale for X/Y charts */ - function drawXYScale($Data,$DataDescription,$YSerieName,$XSerieName,$R,$G,$B,$WithMargin=0,$Angle=0,$Decimals=1) - { - /* Validate the Data and DataDescription array */ - $this->validateData("drawScale",$Data); - - $C_TextColor =$this->AllocateColor($this->Picture,$R,$G,$B); - - $this->drawLine($this->GArea_X1,$this->GArea_Y1,$this->GArea_X1,$this->GArea_Y2,$R,$G,$B); - $this->drawLine($this->GArea_X1,$this->GArea_Y2,$this->GArea_X2,$this->GArea_Y2,$R,$G,$B); - - /* Process Y scale */ - if ( $this->VMin == NULL && $this->VMax == NULL) - { - $this->VMin = $Data[0][$YSerieName]; - $this->VMax = $Data[0][$YSerieName]; - - foreach ( $Data as $Key => $Values ) - { - if (isset($Data[$Key][$YSerieName])) - { - $Value = $Data[$Key][$YSerieName]; - if ( $this->VMax < $Value) { $this->VMax = $Value; } - if ( $this->VMin > $Value) { $this->VMin = $Value; } - } - } - - if ( $this->VMax > preg_replace('/\.[0-9]+/','',$this->VMax) ) - $this->VMax = preg_replace('/\.[0-9]+/','',$this->VMax)+1; - - $DataRange = $this->VMax - $this->VMin; - if ( $DataRange == 0 ) { $DataRange = .1; } - - /* Compute automatic scaling */ - $ScaleOk = FALSE; $Factor = 1; - $MinDivHeight = 25; $MaxDivs = ($this->GArea_Y2 - $this->GArea_Y1) / $MinDivHeight; - - if ( $this->VMin == 0 && $this->VMax == 0 ) - { $this->VMin = 0; $this->VMax = 2; $Scale = 1; $Divisions = 2;} - elseif ($MaxDivs > 1) - { - while(!$ScaleOk) - { - $Scale1 = ( $this->VMax - $this->VMin ) / $Factor; - $Scale2 = ( $this->VMax - $this->VMin ) / $Factor / 2; - $Scale4 = ( $this->VMax - $this->VMin ) / $Factor / 4; - - if ( $Scale1 > 1 && $Scale1 <= $MaxDivs && !$ScaleOk) { $ScaleOk = TRUE; $Divisions = floor($Scale1); $Scale = 1;} - if ( $Scale2 > 1 && $Scale2 <= $MaxDivs && !$ScaleOk) { $ScaleOk = TRUE; $Divisions = floor($Scale2); $Scale = 2;} - if (!$ScaleOk) - { - if ( $Scale2 > 1 ) { $Factor = $Factor * 10; } - if ( $Scale2 < 1 ) { $Factor = $Factor / 10; } - } - } - - if ( floor($this->VMax / $Scale / $Factor) != $this->VMax / $Scale / $Factor) - { - $GridID = floor ( $this->VMax / $Scale / $Factor) + 1; - $this->VMax = $GridID * $Scale * $Factor; - $Divisions++; - } - - if ( floor($this->VMin / $Scale / $Factor) != $this->VMin / $Scale / $Factor) - { - $GridID = floor( $this->VMin / $Scale / $Factor); - $this->VMin = $GridID * $Scale * $Factor; - $Divisions++; - } - } - else /* Can occurs for small graphs */ - $Scale = 1; - - if ( !isset($Divisions) ) - $Divisions = 2; - - if ( $this->isRealInt(($this->VMax-$this->VMin)/($Divisions-1))) - $Divisions--; - elseif ( $this->isRealInt(($this->VMax-$this->VMin)/($Divisions+1))) - $Divisions++; - } - else - $Divisions = $this->Divisions; - - $this->DivisionCount = $Divisions; - - $DataRange = $this->VMax - $this->VMin; - if ( $DataRange == 0 ) { $DataRange = .1; } - - $this->DivisionHeight = ( $this->GArea_Y2 - $this->GArea_Y1 ) / $Divisions; - $this->DivisionRatio = ( $this->GArea_Y2 - $this->GArea_Y1 ) / $DataRange; - - $YPos = $this->GArea_Y2; $XMin = NULL; - for($i=1;$i<=$Divisions+1;$i++) - { - $this->drawLine($this->GArea_X1,$YPos,$this->GArea_X1-5,$YPos,$R,$G,$B); - $Value = $this->VMin + ($i-1) * (( $this->VMax - $this->VMin ) / $Divisions); - $Value = round($Value * pow(10,$Decimals)) / pow(10,$Decimals); - if ( $DataDescription["Format"]["Y"] == "number" ) - $Value = $Value.$DataDescription["Unit"]["Y"]; - if ( $DataDescription["Format"]["Y"] == "time" ) - $Value = $this->ToTime($Value); - if ( $DataDescription["Format"]["Y"] == "date" ) - $Value = $this->ToDate($Value); - if ( $DataDescription["Format"]["Y"] == "metric" ) - $Value = $this->ToMetric($Value); - if ( $DataDescription["Format"]["Y"] == "currency" ) - $Value = $this->ToCurrency($Value); - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextWidth = $Position[2]-$Position[0]; - imagettftext($this->Picture,$this->FontSize,0,$this->GArea_X1-10-$TextWidth,$YPos+($this->FontSize/2),$C_TextColor,$this->FontName,$Value); - - if ( $XMin > $this->GArea_X1-10-$TextWidth || $XMin == NULL ) { $XMin = $this->GArea_X1-10-$TextWidth; } - - $YPos = $YPos - $this->DivisionHeight; - } - - /* Process X scale */ - if ( $this->VXMin == NULL && $this->VXMax == NULL) - { - $this->VXMin = $Data[0][$XSerieName]; - $this->VXMax = $Data[0][$XSerieName]; - - foreach ( $Data as $Key => $Values ) - { - if (isset($Data[$Key][$XSerieName])) - { - $Value = $Data[$Key][$XSerieName]; - if ( $this->VXMax < $Value) { $this->VXMax = $Value; } - if ( $this->VXMin > $Value) { $this->VXMin = $Value; } - } - } - - if ( $this->VXMax > preg_replace('/\.[0-9]+/','',$this->VXMax) ) - $this->VXMax = preg_replace('/\.[0-9]+/','',$this->VXMax)+1; - - $DataRange = $this->VMax - $this->VMin; - if ( $DataRange == 0 ) { $DataRange = .1; } - - /* Compute automatic scaling */ - $ScaleOk = FALSE; $Factor = 1; - $MinDivWidth = 25; $MaxDivs = ($this->GArea_X2 - $this->GArea_X1) / $MinDivWidth; - - if ( $this->VXMin == 0 && $this->VXMax == 0 ) - { $this->VXMin = 0; $this->VXMax = 2; $Scale = 1; $XDivisions = 2;} - elseif ($MaxDivs > 1) - { - while(!$ScaleOk) - { - $Scale1 = ( $this->VXMax - $this->VXMin ) / $Factor; - $Scale2 = ( $this->VXMax - $this->VXMin ) / $Factor / 2; - $Scale4 = ( $this->VXMax - $this->VXMin ) / $Factor / 4; - - if ( $Scale1 > 1 && $Scale1 <= $MaxDivs && !$ScaleOk) { $ScaleOk = TRUE; $XDivisions = floor($Scale1); $Scale = 1;} - if ( $Scale2 > 1 && $Scale2 <= $MaxDivs && !$ScaleOk) { $ScaleOk = TRUE; $XDivisions = floor($Scale2); $Scale = 2;} - if (!$ScaleOk) - { - if ( $Scale2 > 1 ) { $Factor = $Factor * 10; } - if ( $Scale2 < 1 ) { $Factor = $Factor / 10; } - } - } - - if ( floor($this->VXMax / $Scale / $Factor) != $this->VXMax / $Scale / $Factor) - { - $GridID = floor ( $this->VXMax / $Scale / $Factor) + 1; - $this->VXMax = $GridID * $Scale * $Factor; - $XDivisions++; - } - - if ( floor($this->VXMin / $Scale / $Factor) != $this->VXMin / $Scale / $Factor) - { - $GridID = floor( $this->VXMin / $Scale / $Factor); - $this->VXMin = $GridID * $Scale * $Factor; - $XDivisions++; - } - } - else /* Can occurs for small graphs */ - $Scale = 1; - - if ( !isset($XDivisions) ) - $XDivisions = 2; - - if ( $this->isRealInt(($this->VXMax-$this->VXMin)/($XDivisions-1))) - $XDivisions--; - elseif ( $this->isRealInt(($this->VXMax-$this->VXMin)/($XDivisions+1))) - $XDivisions++; - } - else - $XDivisions = $this->XDivisions; - - $this->XDivisionCount = $Divisions; - $this->DataCount = $Divisions + 2; - - $XDataRange = $this->VXMax - $this->VXMin; - if ( $XDataRange == 0 ) { $XDataRange = .1; } - - $this->DivisionWidth = ( $this->GArea_X2 - $this->GArea_X1 ) / $XDivisions; - $this->XDivisionRatio = ( $this->GArea_X2 - $this->GArea_X1 ) / $XDataRange; - - $XPos = $this->GArea_X1; $YMax = NULL; - for($i=1;$i<=$XDivisions+1;$i++) - { - $this->drawLine($XPos,$this->GArea_Y2,$XPos,$this->GArea_Y2+5,$R,$G,$B); - - $Value = $this->VXMin + ($i-1) * (( $this->VXMax - $this->VXMin ) / $XDivisions); - $Value = round($Value * pow(10,$Decimals)) / pow(10,$Decimals); - if ( $DataDescription["Format"]["Y"] == "number" ) - $Value = $Value.$DataDescription["Unit"]["Y"]; - if ( $DataDescription["Format"]["Y"] == "time" ) - $Value = $this->ToTime($Value); - if ( $DataDescription["Format"]["Y"] == "date" ) - $Value = $this->ToDate($Value); - if ( $DataDescription["Format"]["Y"] == "metric" ) - $Value = $this->ToMetric($Value); - if ( $DataDescription["Format"]["Y"] == "currency" ) - $Value = $this->ToCurrency($Value); - - $Position = imageftbbox($this->FontSize,$Angle,$this->FontName,$Value); - $TextWidth = abs($Position[2])+abs($Position[0]); - $TextHeight = abs($Position[1])+abs($Position[3]); - - if ( $Angle == 0 ) - { - $YPos = $this->GArea_Y2+18; - imagettftext($this->Picture,$this->FontSize,$Angle,floor($XPos)-floor($TextWidth/2),$YPos,$C_TextColor,$this->FontName,$Value); - } - else - { - $YPos = $this->GArea_Y2+10+$TextHeight; - if ( $Angle <= 90 ) - imagettftext($this->Picture,$this->FontSize,$Angle,floor($XPos)-$TextWidth+5,$YPos,$C_TextColor,$this->FontName,$Value); - else - imagettftext($this->Picture,$this->FontSize,$Angle,floor($XPos)+$TextWidth+5,$YPos,$C_TextColor,$this->FontName,$Value); - } - - if ( $YMax < $YPos || $YMax == NULL ) { $YMax = $YPos; } - - $XPos = $XPos + $this->DivisionWidth; - } - - /* Write the Y Axis caption if set */ - if ( isset($DataDescription["Axis"]["Y"]) ) - { - $Position = imageftbbox($this->FontSize,90,$this->FontName,$DataDescription["Axis"]["Y"]); - $TextHeight = abs($Position[1])+abs($Position[3]); - $TextTop = (($this->GArea_Y2 - $this->GArea_Y1) / 2) + $this->GArea_Y1 + ($TextHeight/2); - imagettftext($this->Picture,$this->FontSize,90,$XMin-$this->FontSize,$TextTop,$C_TextColor,$this->FontName,$DataDescription["Axis"]["Y"]); - } - - /* Write the X Axis caption if set */ - if ( isset($DataDescription["Axis"]["X"]) ) - { - $Position = imageftbbox($this->FontSize,90,$this->FontName,$DataDescription["Axis"]["X"]); - $TextWidth = abs($Position[2])+abs($Position[0]); - $TextLeft = (($this->GArea_X2 - $this->GArea_X1) / 2) + $this->GArea_X1 + ($TextWidth/2); - imagettftext($this->Picture,$this->FontSize,0,$TextLeft,$YMax+$this->FontSize+5,$C_TextColor,$this->FontName,$DataDescription["Axis"]["X"]); - } - } - - /* Compute and draw the scale */ - function drawGrid($LineWidth,$Mosaic=TRUE,$R=220,$G=220,$B=220,$Alpha=100) - { - /* Draw mosaic */ - if ( $Mosaic ) - { - $LayerWidth = $this->GArea_X2-$this->GArea_X1; - $LayerHeight = $this->GArea_Y2-$this->GArea_Y1; - - $this->Layers[0] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White =$this->AllocateColor($this->Layers[0],255,255,255); - imagefilledrectangle($this->Layers[0],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[0],$C_White); - - $C_Rectangle =$this->AllocateColor($this->Layers[0],250,250,250); - - $YPos = $LayerHeight; //$this->GArea_Y2-1; - $LastY = $YPos; - for($i=0;$i<=$this->DivisionCount;$i++) - { - $LastY = $YPos; - $YPos = $YPos - $this->DivisionHeight; - - if ( $YPos <= 0 ) { $YPos = 1; } - - if ( $i % 2 == 0 ) - { - imagefilledrectangle($this->Layers[0],1,$YPos,$LayerWidth-1,$LastY,$C_Rectangle); - } - } - imagecopymerge($this->Picture,$this->Layers[0],$this->GArea_X1,$this->GArea_Y1,0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[0]); - } - - /* Horizontal lines */ - $YPos = $this->GArea_Y2 - $this->DivisionHeight; - for($i=1;$i<=$this->DivisionCount;$i++) - { - if ( $YPos > $this->GArea_Y1 && $YPos < $this->GArea_Y2 ) - $this->drawDottedLine($this->GArea_X1,$YPos,$this->GArea_X2,$YPos,$LineWidth,$R,$G,$B); - - $YPos = $YPos - $this->DivisionHeight; - } - - /* Vertical lines */ - if ( $this->GAreaXOffset == 0 ) - { $XPos = $this->GArea_X1 + $this->DivisionWidth + $this->GAreaXOffset; $ColCount = $this->DataCount-2; } - else - { $XPos = $this->GArea_X1 + $this->GAreaXOffset; $ColCount = floor( ($this->GArea_X2 - $this->GArea_X1) / $this->DivisionWidth ); } - - for($i=1;$i<=$ColCount;$i++) - { - if ( $XPos > $this->GArea_X1 && $XPos < $this->GArea_X2 ) - $this->drawDottedLine(floor($XPos),$this->GArea_Y1,floor($XPos),$this->GArea_Y2,$LineWidth,$R,$G,$B); - $XPos = $XPos + $this->DivisionWidth; - } - } - - /* retrieve the legends size */ - function getLegendBoxSize($DataDescription) - { - if ( !isset($DataDescription["Description"]) ) - return(-1); - - /* <-10->[8]<-4->Text<-10-> */ - $MaxWidth = 0; $MaxHeight = 8; - foreach($DataDescription["Description"] as $Key => $Value) - { - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = $Position[1]-$Position[7]; - if ( $TextWidth > $MaxWidth) { $MaxWidth = $TextWidth; } - $MaxHeight = $MaxHeight + $TextHeight + 4; - } - $MaxHeight = $MaxHeight - 3; - $MaxWidth = $MaxWidth + 32; - - return(array($MaxWidth,$MaxHeight)); - } - - function getPieLegendBoxSize($Data) - { - $MaxWidth = 0; $MaxHeight = 8; - foreach($Data as $Value) - { - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value['Keys']); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = $Position[1]-$Position[7]; - if ( $TextWidth > $MaxWidth) { $MaxWidth = $TextWidth; } - $MaxHeight = $MaxHeight + $TextHeight + 4; - } - $MaxHeight = $MaxHeight - 3; - $MaxWidth = $MaxWidth + 32; - - return(array($MaxWidth,$MaxHeight)); - } - - /* Draw the data legends */ - function drawLegend($XPos,$YPos,$DataDescription,$R,$G,$B,$Rs=-1,$Gs=-1,$Bs=-1,$Rt=0,$Gt=0,$Bt=0,$Border=TRUE) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawLegend",$DataDescription); - - if ( !isset($DataDescription["Description"]) ) - return(-1); - - $C_TextColor =$this->AllocateColor($this->Picture,$Rt,$Gt,$Bt); - - /* <-10->[8]<-4->Text<-10-> */ - $MaxWidth = 0; $MaxHeight = 8; - foreach($DataDescription["Description"] as $Key => $Value) - { - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = $Position[1]-$Position[7]; - if ( $TextWidth > $MaxWidth) { $MaxWidth = $TextWidth; } - $MaxHeight = $MaxHeight + $TextHeight + 4; - } - $MaxHeight = $MaxHeight - 5; - $MaxWidth = $MaxWidth + 32; - - if ( $Rs == -1 || $Gs == -1 || $Bs == -1 ) - { $Rs = $R-30; $Gs = $G-30; $Bs = $B-30; } - - if ( $Border ) - { - $this->drawFilledRoundedRectangle($XPos+1,$YPos+1,$XPos+$MaxWidth+1,$YPos+$MaxHeight+1,5,$Rs,$Gs,$Bs); - $this->drawFilledRoundedRectangle($XPos,$YPos,$XPos+$MaxWidth,$YPos+$MaxHeight,5,$R,$G,$B); - } - - $YOffset = 4 + $this->FontSize; $ID = 0; - foreach($DataDescription["Description"] as $Key => $Value) - { - $this->drawFilledRoundedRectangle($XPos+10,$YPos+$YOffset-4,$XPos+14,$YPos+$YOffset-4,2,$this->Palette[$ID]["R"],$this->Palette[$ID]["G"],$this->Palette[$ID]["B"]); - imagettftext($this->Picture,$this->FontSize,0,$XPos+22,$YPos+$YOffset,$C_TextColor,$this->FontName,$Value); - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextHeight = $Position[1]-$Position[7]; - - $YOffset = $YOffset + $TextHeight + 4; - $ID++; - } - } - - /* Draw the data legends */ - function drawPieLegend($XPos,$YPos,$Data,$DataDescription,$R,$G,$B) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawPieLegend",$DataDescription,FALSE); - $this->validateData("drawPieLegend",$Data); - - if ( !isset($DataDescription["Position"]) ) - return(-1); - - $C_TextColor =$this->AllocateColor($this->Picture,0,0,0); - - /* <-10->[8]<-4->Text<-10-> */ - $MaxWidth = 0; $MaxHeight = 8; - foreach($Data as $Key => $Value) - { - $Value2 = $Value[$DataDescription["Position"]]; - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value2); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = $Position[1]-$Position[7]; - if ( $TextWidth > $MaxWidth) { $MaxWidth = $TextWidth; } - - $MaxHeight = $MaxHeight + $TextHeight + 4; - } - $MaxHeight = $MaxHeight - 3; - $MaxWidth = $MaxWidth + 32; - - $this->drawFilledRoundedRectangle($XPos+1,$YPos+1,$XPos+$MaxWidth+1,$YPos+$MaxHeight+1,5,$R-30,$G-30,$B-30); - $this->drawFilledRoundedRectangle($XPos,$YPos,$XPos+$MaxWidth,$YPos+$MaxHeight,5,$R,$G,$B); - - $YOffset = 4 + $this->FontSize; $ID = 0; - foreach($Data as $Key => $Value) - { - $Value2 = $Value[$DataDescription["Position"]]; - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value2); - $TextHeight = $Position[1]-$Position[7]; - $this->drawFilledRectangle($XPos+10,$YPos+$YOffset-6,$XPos+14,$YPos+$YOffset-2,$this->Palette[$ID]["R"],$this->Palette[$ID]["G"],$this->Palette[$ID]["B"]); - - imagettftext($this->Picture,$this->FontSize,0,$XPos+22,$YPos+$YOffset,$C_TextColor,$this->FontName,$Value2); - $YOffset = $YOffset + $TextHeight + 4; - $ID++; - } - } - - /* Draw the graph title */ - function drawTitle($XPos,$YPos,$Value,$R,$G,$B,$XPos2=-1,$YPos2=-1,$Shadow=FALSE) - { - $C_TextColor = $this->AllocateColor($this->Picture,$R,$G,$B); - - if ( $XPos2 != -1 ) - { - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextWidth = $Position[2]-$Position[0]; - $XPos = floor(( $XPos2 - $XPos - $TextWidth ) / 2 ) + $XPos; - } - - if ( $YPos2 != -1 ) - { - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Value); - $TextHeight = $Position[5]-$Position[3]; - $YPos = floor(( $YPos2 - $YPos - $TextHeight ) / 2 ) + $YPos; - } - - if ( $Shadow ) - { - $C_ShadowColor = $this->AllocateColor($this->Picture,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor); - imagettftext($this->Picture,$this->FontSize,0,$XPos+$this->ShadowXDistance,$YPos+$this->ShadowYDistance,$C_ShadowColor,$this->FontName,$Value); - } - - imagettftext($this->Picture,$this->FontSize,0,$XPos,$YPos,$C_TextColor,$this->FontName,$Value); - } - - /* Draw a text box with text align & alpha properties */ - function drawTextBox($X1,$Y1,$X2,$Y2,$Text,$Angle=0,$R=255,$G=255,$B=255,$Align=ALIGN_LEFT,$Shadow=TRUE,$BgR=-1,$BgG=-1,$BgB=-1,$Alpha=100) - { - $Position = imageftbbox($this->FontSize,$Angle,$this->FontName,$Text); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = $Position[5]-$Position[3]; - $AreaWidth = $X2 - $X1; - $AreaHeight = $Y2 - $Y1; - - if ( $BgR != -1 && $BgG != -1 && $BgB != -1 ) - $this->drawFilledRectangle($X1,$Y1,$X2,$Y2,$BgR,$BgG,$BgB,FALSE,$Alpha); - - if ( $Align == ALIGN_TOP_LEFT ) { $X = $X1+1; $Y = $Y1+$this->FontSize+1; } - if ( $Align == ALIGN_TOP_CENTER ) { $X = $X1+($AreaWidth/2)-($TextWidth/2); $Y = $Y1+$this->FontSize+1; } - if ( $Align == ALIGN_TOP_RIGHT ) { $X = $X2-$TextWidth-1; $Y = $Y1+$this->FontSize+1; } - if ( $Align == ALIGN_LEFT ) { $X = $X1+1; $Y = $Y1+($AreaHeight/2)-($TextHeight/2); } - if ( $Align == ALIGN_CENTER ) { $X = $X1+($AreaWidth/2)-($TextWidth/2); $Y = $Y1+($AreaHeight/2)-($TextHeight/2); } - if ( $Align == ALIGN_RIGHT ) { $X = $X2-$TextWidth-1; $Y = $Y1+($AreaHeight/2)-($TextHeight/2); } - if ( $Align == ALIGN_BOTTOM_LEFT ) { $X = $X1+1; $Y = $Y2-1; } - if ( $Align == ALIGN_BOTTOM_CENTER ) { $X = $X1+($AreaWidth/2)-($TextWidth/2); $Y = $Y2-1; } - if ( $Align == ALIGN_BOTTOM_RIGHT ) { $X = $X2-$TextWidth-1; $Y = $Y2-1; } - - $C_TextColor =$this->AllocateColor($this->Picture,$R,$G,$B); - $C_ShadowColor =$this->AllocateColor($this->Picture,0,0,0); - if ( $Shadow ) - imagettftext($this->Picture,$this->FontSize,$Angle,$X+1,$Y+1,$C_ShadowColor,$this->FontName,$Text); - - imagettftext($this->Picture,$this->FontSize,$Angle,$X,$Y,$C_TextColor,$this->FontName,$Text); - } - - /* Compute and draw the scale */ - function drawTreshold($Value,$R,$G,$B,$ShowLabel=FALSE,$ShowOnRight=FALSE,$TickWidth=4,$FreeText=NULL) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_TextColor =$this->AllocateColor($this->Picture,$R,$G,$B); - $Y = $this->GArea_Y2 - ($Value - $this->VMin) * $this->DivisionRatio; - - if ( $Y <= $this->GArea_Y1 || $Y >= $this->GArea_Y2 ) - return(-1); - - if ( $TickWidth == 0 ) - $this->drawLine($this->GArea_X1,$Y,$this->GArea_X2,$Y,$R,$G,$B); - else - $this->drawDottedLine($this->GArea_X1,$Y,$this->GArea_X2,$Y,$TickWidth,$R,$G,$B); - - if ( $ShowLabel ) - { - if ( $FreeText == NULL ) - { $Label = $Value; } else { $Label = $FreeText; } - - if ( $ShowOnRight ) - imagettftext($this->Picture,$this->FontSize,0,$this->GArea_X2+2,$Y+($this->FontSize/2),$C_TextColor,$this->FontName,$Label); - else - imagettftext($this->Picture,$this->FontSize,0,$this->GArea_X1+2,$Y-($this->FontSize/2),$C_TextColor,$this->FontName,$Label); - } - } - - /* This function put a label on a specific point */ - function setLabel($Data,$DataDescription,$SerieName,$ValueName,$Caption,$R=210,$G=210,$B=210) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("setLabel",$DataDescription); - $this->validateData("setLabel",$Data); - $ShadowFactor = 100; - $C_Label =$this->AllocateColor($this->Picture,$R,$G,$B); - $C_Shadow =$this->AllocateColor($this->Picture,$R-$ShadowFactor,$G-$ShadowFactor,$B-$ShadowFactor); - $C_TextColor =$this->AllocateColor($this->Picture,0,0,0); - - $Cp = 0; $Found = FALSE; - foreach ( $Data as $Key => $Value ) - { - if ( $Data[$Key][$DataDescription["Position"]] == $ValueName ) - { $NumericalValue = $Data[$Key][$SerieName]; $Found = TRUE; } - if ( !$Found ) - $Cp++; - } - - $XPos = $this->GArea_X1 + $this->GAreaXOffset + ( $this->DivisionWidth * $Cp ) + 2; - $YPos = $this->GArea_Y2 - ($NumericalValue - $this->VMin) * $this->DivisionRatio; - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Caption); - $TextHeight = $Position[3] - $Position[5]; - $TextWidth = $Position[2]-$Position[0] + 2; - $TextOffset = floor($TextHeight/2); - - // Shadow - $Poly = array($XPos+1,$YPos+1,$XPos + 9,$YPos - $TextOffset,$XPos + 8,$YPos + $TextOffset + 2); - imagefilledpolygon($this->Picture,$Poly,3,$C_Shadow); - $this->drawLine($XPos,$YPos+1,$XPos + 9,$YPos - $TextOffset - .2,$R-$ShadowFactor,$G-$ShadowFactor,$B-$ShadowFactor); - $this->drawLine($XPos,$YPos+1,$XPos + 9,$YPos + $TextOffset + 2.2,$R-$ShadowFactor,$G-$ShadowFactor,$B-$ShadowFactor); - $this->drawFilledRectangle($XPos + 9,$YPos - $TextOffset-.2,$XPos + 13 + $TextWidth,$YPos + $TextOffset + 2.2,$R-$ShadowFactor,$G-$ShadowFactor,$B-$ShadowFactor); - - // Label background - $Poly = array($XPos,$YPos,$XPos + 8,$YPos - $TextOffset - 1,$XPos + 8,$YPos + $TextOffset + 1); - imagefilledpolygon($this->Picture,$Poly,3,$C_Label); - $this->drawLine($XPos-1,$YPos,$XPos + 8,$YPos - $TextOffset - 1.2,$R,$G,$B); - $this->drawLine($XPos-1,$YPos,$XPos + 8,$YPos + $TextOffset + 1.2,$R,$G,$B); - $this->drawFilledRectangle($XPos + 8,$YPos - $TextOffset - 1.2,$XPos + 12 + $TextWidth,$YPos + $TextOffset + 1.2,$R,$G,$B); - - imagettftext($this->Picture,$this->FontSize,0,$XPos + 10,$YPos + $TextOffset,$C_TextColor,$this->FontName,$Caption); - } - - /* This function draw a plot graph */ - function drawPlotGraph($Data,$DataDescription,$BigRadius=5,$SmallRadius=2,$R2=-1,$G2=-1,$B2=-1,$Shadow=FALSE) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawPlotGraph",$DataDescription); - $this->validateData("drawPlotGraph",$Data); - - $GraphID = 0; - $Ro = $R2; $Go = $G2; $Bo = $B2; - - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $R = $this->Palette[$ColorID]["R"]; - $G = $this->Palette[$ColorID]["G"]; - $B = $this->Palette[$ColorID]["B"]; - $R2 = $Ro; $G2 = $Go; $B2 = $Bo; - - if ( isset($DataDescription["Symbol"][$ColName]) ) - { - $Is_Alpha = ((ord ( file_get_contents ($DataDescription["Symbol"][$ColName], false, null, 25, 1)) & 6) & 4) == 4; - - $Infos = getimagesize($DataDescription["Symbol"][$ColName]); - $ImageWidth = $Infos[0]; - $ImageHeight = $Infos[1]; - $Symbol = imagecreatefromgif($DataDescription["Symbol"][$ColName]); - } - - $XPos = $this->GArea_X1 + $this->GAreaXOffset; - $Hsize = round($BigRadius/2); - $R3 = -1; $G3 = -1; $B3 = -1; - foreach ( $Data as $Key => $Values ) - { - $Value = $Data[$Key][$ColName]; - $YPos = $this->GArea_Y2 - (($Value-$this->VMin) * $this->DivisionRatio); - - /* Save point into the image map if option activated */ - if ( $this->BuildMap ) - $this->addToImageMap($XPos-$Hsize,$YPos-$Hsize,$XPos+1+$Hsize,$YPos+$Hsize+1,$DataDescription["Description"][$ColName],$Data[$Key][$ColName].$DataDescription["Unit"]["Y"],"Plot"); - - if ( is_numeric($Value) ) - { - if ( !isset($DataDescription["Symbol"][$ColName]) ) - { - - if ( $Shadow ) - { - if ( $R3 !=-1 && $G3 !=-1 && $B3 !=-1 ) - $this->drawFilledCircle($XPos+2,$YPos+2,$BigRadius,$R3,$G3,$B3); - else - { - $R3 = $this->Palette[$ColorID]["R"]-20; if ( $R3 < 0 ) { $R3 = 0; } - $G3 = $this->Palette[$ColorID]["G"]-20; if ( $G3 < 0 ) { $G3 = 0; } - $B3 = $this->Palette[$ColorID]["B"]-20; if ( $B3 < 0 ) { $B3 = 0; } - $this->drawFilledCircle($XPos+2,$YPos+2,$BigRadius,$R3,$G3,$B3); - } - } - - $this->drawFilledCircle($XPos+1,$YPos+1,$BigRadius,$R,$G,$B); - - if ( $SmallRadius != 0 ) - { - if ( $R2 !=-1 && $G2 !=-1 && $B2 !=-1 ) - $this->drawFilledCircle($XPos+1,$YPos+1,$SmallRadius,$R2,$G2,$B2); - else - { - $R2 = $this->Palette[$ColorID]["R"]-15; if ( $R2 < 0 ) { $R2 = 0; } - $G2 = $this->Palette[$ColorID]["G"]-15; if ( $G2 < 0 ) { $G2 = 0; } - $B2 = $this->Palette[$ColorID]["B"]-15; if ( $B2 < 0 ) { $B2 = 0; } - - $this->drawFilledCircle($XPos+1,$YPos+1,$SmallRadius,$R2,$G2,$B2); - } - } - } - else - { - imagecopymerge($this->Picture,$Symbol,$XPos+1-$ImageWidth/2,$YPos+1-$ImageHeight/2,0,0,$ImageWidth,$ImageHeight,100); - } - } - - $XPos = $XPos + $this->DivisionWidth; - } - $GraphID++; - } - } - - /* This function draw a plot graph in an X/Y space */ - function drawXYPlotGraph($Data,$DataDescription,$YSerieName,$XSerieName,$PaletteID=0,$BigRadius=5,$SmallRadius=2,$R2=-1,$G2=-1,$B2=-1,$Shadow=TRUE) - { - $R = $this->Palette[$PaletteID]["R"]; - $G = $this->Palette[$PaletteID]["G"]; - $B = $this->Palette[$PaletteID]["B"]; - $R3 = -1; $G3 = -1; $B3 = -1; - - $YLast = -1; $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$YSerieName]) && isset($Data[$Key][$XSerieName]) ) - { - $X = $Data[$Key][$XSerieName]; - $Y = $Data[$Key][$YSerieName]; - - $Y = $this->GArea_Y2 - (($Y-$this->VMin) * $this->DivisionRatio); - $X = $this->GArea_X1 + (($X-$this->VXMin) * $this->XDivisionRatio); - - - if ( $Shadow ) - { - if ( $R3 !=-1 && $G3 !=-1 && $B3 !=-1 ) - $this->drawFilledCircle($X+2,$Y+2,$BigRadius,$R3,$G3,$B3); - else - { - $R3 = $this->Palette[$PaletteID]["R"]-20; if ( $R < 0 ) { $R = 0; } - $G3 = $this->Palette[$PaletteID]["G"]-20; if ( $G < 0 ) { $G = 0; } - $B3 = $this->Palette[$PaletteID]["B"]-20; if ( $B < 0 ) { $B = 0; } - $this->drawFilledCircle($X+2,$Y+2,$BigRadius,$R3,$G3,$B3); - } - } - - $this->drawFilledCircle($X+1,$Y+1,$BigRadius,$R,$G,$B); - - if ( $R2 !=-1 && $G2 !=-1 && $B2 !=-1 ) - $this->drawFilledCircle($X+1,$Y+1,$SmallRadius,$R2,$G2,$B2); - else - { - $R2 = $this->Palette[$PaletteID]["R"]+20; if ( $R > 255 ) { $R = 255; } - $G2 = $this->Palette[$PaletteID]["G"]+20; if ( $G > 255 ) { $G = 255; } - $B2 = $this->Palette[$PaletteID]["B"]+20; if ( $B > 255 ) { $B = 255; } - $this->drawFilledCircle($X+1,$Y+1,$SmallRadius,$R2,$G2,$B2); - } - } - } - - } - - /* This function draw an area between two series */ - function drawArea($Data,$Serie1,$Serie2,$R,$G,$B,$Alpha = 50) - { - /* Validate the Data and DataDescription array */ - $this->validateData("drawArea",$Data); - - $LayerWidth = $this->GArea_X2-$this->GArea_X1; - $LayerHeight = $this->GArea_Y2-$this->GArea_Y1; - - $this->Layers[0] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White =$this->AllocateColor($this->Layers[0],255,255,255); - imagefilledrectangle($this->Layers[0],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[0],$C_White); - - $C_Graph =$this->AllocateColor($this->Layers[0],$R,$G,$B); - - $XPos = $this->GAreaXOffset; - $LastXPos = -1; - foreach ( $Data as $Key => $Values ) - { - $Value1 = $Data[$Key][$Serie1]; - $Value2 = $Data[$Key][$Serie2]; - $YPos1 = $LayerHeight - (($Value1-$this->VMin) * $this->DivisionRatio); - $YPos2 = $LayerHeight - (($Value2-$this->VMin) * $this->DivisionRatio); - - if ( $LastXPos != -1 ) - { - $Points = ""; - $Points[] = $LastXPos; $Points[] = $LastYPos1; - $Points[] = $LastXPos; $Points[] = $LastYPos2; - $Points[] = $XPos; $Points[] = $YPos2; - $Points[] = $XPos; $Points[] = $YPos1; - - imagefilledpolygon($this->Layers[0],$Points,4,$C_Graph); - } - - $LastYPos1 = $YPos1; - $LastYPos2 = $YPos2; - $LastXPos = $XPos; - - $XPos = $XPos + $this->DivisionWidth; - } - - imagecopymerge($this->Picture,$this->Layers[0],$this->GArea_X1,$this->GArea_Y1,0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[0]); - } - - - /* This function write the values of the specified series */ - function writeValues($Data,$DataDescription,$Series) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("writeValues",$DataDescription); - $this->validateData("writeValues",$Data); - - if ( !is_array($Series) ) { $Series = array($Series); } - - foreach($Series as $Key => $Serie) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $Serie ) { $ColorID = $ID; }; $ID++; } - - $XPos = $this->GArea_X1 + $this->GAreaXOffset; - $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$Serie]) && is_numeric($Data[$Key][$Serie])) - { - $Value = $Data[$Key][$Serie]; - $YPos = $this->GArea_Y2 - (($Value-$this->VMin) * $this->DivisionRatio); - - $Positions = imagettfbbox($this->FontSize,0,$this->FontName,$Value); - $Width = $Positions[2] - $Positions[6]; $XOffset = $XPos - ($Width/2); - $Height = $Positions[3] - $Positions[7]; $YOffset = $YPos - 4; - - $C_TextColor =$this->AllocateColor($this->Picture,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagettftext($this->Picture,$this->FontSize,0,$XOffset,$YOffset,$C_TextColor,$this->FontName,$Value); - } - $XPos = $XPos + $this->DivisionWidth; - } - - } - } - - /* This function draw a line graph */ - function drawLineGraph($Data,$DataDescription,$SerieName="") - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawLineGraph",$DataDescription); - $this->validateData("drawLineGraph",$Data); - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - if ( $SerieName == "" || $SerieName == $ColName ) - { - $XPos = $this->GArea_X1 + $this->GAreaXOffset; - $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - { - $Value = $Data[$Key][$ColName]; - $YPos = $this->GArea_Y2 - (($Value-$this->VMin) * $this->DivisionRatio); - - /* Save point into the image map if option activated */ - if ( $this->BuildMap ) - $this->addToImageMap($XPos-3,$YPos-3,$XPos+3,$YPos+3,$DataDescription["Description"][$ColName],$Data[$Key][$ColName].$DataDescription["Unit"]["Y"],"Line"); - - if (!is_numeric($Value)) { $XLast = -1; } - if ( $XLast != -1 ) - $this->drawLine($XLast,$YLast,$XPos,$YPos,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"],TRUE); - - $XLast = $XPos; - $YLast = $YPos; - if (!is_numeric($Value)) { $XLast = -1; } - } - $XPos = $XPos + $this->DivisionWidth; - } - $GraphID++; - } - } - } - - /* This function draw a line graph */ - function drawXYGraph($Data,$DataDescription,$YSerieName,$XSerieName,$PaletteID=0) - { - $YLast = -1; $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$YSerieName]) && isset($Data[$Key][$XSerieName]) ) - { - $X = $Data[$Key][$XSerieName]; - $Y = $Data[$Key][$YSerieName]; - - $Y = $this->GArea_Y2 - (($Y-$this->VMin) * $this->DivisionRatio); - $X = $this->GArea_X1 + (($X-$this->VXMin) * $this->XDivisionRatio); - - if ($XLast != -1 && $YLast != -1) - { - $this->drawLine($XLast,$YLast,$X,$Y,$this->Palette[$PaletteID]["R"],$this->Palette[$PaletteID]["G"],$this->Palette[$PaletteID]["B"],TRUE); - } - - $XLast = $X; - $YLast = $Y; - } - } - } - - /* This function draw a cubic curve */ - function drawCubicCurve($Data,$DataDescription,$Accuracy=.1,$SerieName="") - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawCubicCurve",$DataDescription); - $this->validateData("drawCubicCurve",$Data); - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if ( $SerieName == "" || $SerieName == $ColName ) - { - $XIn = ""; $Yin = ""; $Yt = ""; $U = ""; - $XIn[0] = 0; $YIn[0] = 0; - - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $Index = 1; - $XLast = -1; $Missing = ""; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName]) ) - { - $Value = $Data[$Key][$ColName]; - $XIn[$Index] = $Index; - $YIn[$Index] = $Value; - if ( !is_numeric($Value) ) { $Missing[$Index] = TRUE; } - $Index++; - } - } - $Index--; - - $Yt[0] = 0; - $Yt[1] = 0; - $U[1] = 0; - for($i=2;$i<=$Index-1;$i++) - { - $Sig = ($XIn[$i] - $XIn[$i-1]) / ($XIn[$i+1] - $XIn[$i-1]); - $p = $Sig * $Yt[$i-1] + 2; - $Yt[$i] = ($Sig - 1) / $p; - $U[$i] = ($YIn[$i+1] - $YIn[$i]) / ($XIn[$i+1] - $XIn[$i]) - ($YIn[$i] - $YIn[$i-1]) / ($XIn[$i] - $XIn[$i-1]); - $U[$i] = (6 * $U[$i] / ($XIn[$i+1] - $XIn[$i-1]) - $Sig * $U[$i-1]) / $p; - } - - $qn = 0; - $un = 0; - $Yt[$Index] = ($un - $qn * $U[$Index-1]) / ($qn * $Yt[$Index-1] + 1); - - for($k=$Index-1;$k>=1;$k--) - $Yt[$k] = $Yt[$k] * $Yt[$k+1] + $U[$k]; - - $XPos = $this->GArea_X1 + $this->GAreaXOffset; - for($X=1;$X<=$Index;$X=$X+$Accuracy) - { - $klo = 1; - $khi = $Index; - $k = $khi - $klo; - while($k > 1) - { - $k = $khi - $klo; - If ( $XIn[$k] >= $X ) - $khi = $k; - else - $klo = $k; - } - $klo = $khi - 1; - - $h = $XIn[$khi] - $XIn[$klo]; - $a = ($XIn[$khi] - $X) / $h; - $b = ($X - $XIn[$klo]) / $h; - $Value = $a * $YIn[$klo] + $b * $YIn[$khi] + (($a*$a*$a - $a) * $Yt[$klo] + ($b*$b*$b - $b) * $Yt[$khi]) * ($h*$h) / 6; - - $YPos = $this->GArea_Y2 - (($Value-$this->VMin) * $this->DivisionRatio); - - if ( $XLast != -1 && !isset($Missing[floor($X)]) && !isset($Missing[floor($X+1)]) ) - $this->drawLine($XLast,$YLast,$XPos,$YPos,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"],TRUE); - - $XLast = $XPos; - $YLast = $YPos; - $XPos = $XPos + $this->DivisionWidth * $Accuracy; - } - - // Add potentialy missing values - $XPos = $XPos - $this->DivisionWidth * $Accuracy; - if ( $XPos < ($this->GArea_X2 - $this->GAreaXOffset) ) - { - $YPos = $this->GArea_Y2 - (($YIn[$Index]-$this->VMin) * $this->DivisionRatio); - $this->drawLine($XLast,$YLast,$this->GArea_X2-$this->GAreaXOffset,$YPos,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"],TRUE); - } - - $GraphID++; - } - } - } - - /* This function draw a filled cubic curve */ - function drawFilledCubicCurve($Data,$DataDescription,$Accuracy=.1,$Alpha=100,$AroundZero=FALSE) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawFilledCubicCurve",$DataDescription); - $this->validateData("drawFilledCubicCurve",$Data); - - $LayerWidth = $this->GArea_X2-$this->GArea_X1; - $LayerHeight = $this->GArea_Y2-$this->GArea_Y1; - $YZero = $LayerHeight - ((0-$this->VMin) * $this->DivisionRatio); - if ( $YZero > $LayerHeight ) { $YZero = $LayerHeight; } - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $XIn = ""; $Yin = ""; $Yt = ""; $U = ""; - $XIn[0] = 0; $YIn[0] = 0; - - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $Index = 1; - $XLast = -1; $Missing = ""; - foreach ( $Data as $Key => $Values ) - { - $Value = $Data[$Key][$ColName]; - $XIn[$Index] = $Index; - $YIn[$Index] = $Value; - if ( !is_numeric($Value) ) { $Missing[$Index] = TRUE; } - $Index++; - } - $Index--; - - $Yt[0] = 0; - $Yt[1] = 0; - $U[1] = 0; - for($i=2;$i<=$Index-1;$i++) - { - $Sig = ($XIn[$i] - $XIn[$i-1]) / ($XIn[$i+1] - $XIn[$i-1]); - $p = $Sig * $Yt[$i-1] + 2; - $Yt[$i] = ($Sig - 1) / $p; - $U[$i] = ($YIn[$i+1] - $YIn[$i]) / ($XIn[$i+1] - $XIn[$i]) - ($YIn[$i] - $YIn[$i-1]) / ($XIn[$i] - $XIn[$i-1]); - $U[$i] = (6 * $U[$i] / ($XIn[$i+1] - $XIn[$i-1]) - $Sig * $U[$i-1]) / $p; - } - - $qn = 0; - $un = 0; - $Yt[$Index] = ($un - $qn * $U[$Index-1]) / ($qn * $Yt[$Index-1] + 1); - - for($k=$Index-1;$k>=1;$k--) - $Yt[$k] = $Yt[$k] * $Yt[$k+1] + $U[$k]; - - $Points = ""; - $Points[] = $this->GAreaXOffset; - $Points[] = $LayerHeight; - - $this->Layers[0] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White =$this->AllocateColor($this->Layers[0],255,255,255); - imagefilledrectangle($this->Layers[0],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[0],$C_White); - - $YLast = NULL; - $XPos = $this->GAreaXOffset; $PointsCount = 2; - for($X=1;$X<=$Index;$X=$X+$Accuracy) - { - $klo = 1; - $khi = $Index; - $k = $khi - $klo; - while($k > 1) - { - $k = $khi - $klo; - If ( $XIn[$k] >= $X ) - $khi = $k; - else - $klo = $k; - } - $klo = $khi - 1; - - $h = $XIn[$khi] - $XIn[$klo]; - $a = ($XIn[$khi] - $X) / $h; - $b = ($X - $XIn[$klo]) / $h; - $Value = $a * $YIn[$klo] + $b * $YIn[$khi] + (($a*$a*$a - $a) * $Yt[$klo] + ($b*$b*$b - $b) * $Yt[$khi]) * ($h*$h) / 6; - - $YPos = $LayerHeight - (($Value-$this->VMin) * $this->DivisionRatio); - - if ( $YLast != NULL && $AroundZero && !isset($Missing[floor($X)]) && !isset($Missing[floor($X+1)])) - { - $aPoints = ""; - $aPoints[] = $XLast; - $aPoints[] = $YLast; - $aPoints[] = $XPos; - $aPoints[] = $YPos; - $aPoints[] = $XPos; - $aPoints[] = $YZero; - $aPoints[] = $XLast; - $aPoints[] = $YZero; - - $C_Graph =$this->AllocateColor($this->Layers[0],$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagefilledpolygon($this->Layers[0],$aPoints,4,$C_Graph); - } - - if ( !isset($Missing[floor($X)]) || $YLast == NULL ) - { - $PointsCount++; - $Points[] = $XPos; - $Points[] = $YPos; - } - else - { - $PointsCount++; $Points[] = $XLast; $Points[] = $LayerHeight; - } - - $YLast = $YPos; $XLast = $XPos; - $XPos = $XPos + $this->DivisionWidth * $Accuracy; - } - - // Add potentialy missing values - $XPos = $XPos - $this->DivisionWidth * $Accuracy; - if ( $XPos < ($LayerWidth-$this->GAreaXOffset) ) - { - $YPos = $LayerHeight - (($YIn[$Index]-$this->VMin) * $this->DivisionRatio); - - if ( $YLast != NULL && $AroundZero ) - { - $aPoints = ""; - $aPoints[] = $XLast; - $aPoints[] = $YLast; - $aPoints[] = $LayerWidth-$this->GAreaXOffset; - $aPoints[] = $YPos; - $aPoints[] = $LayerWidth-$this->GAreaXOffset; - $aPoints[] = $YZero; - $aPoints[] = $XLast; - $aPoints[] = $YZero; - - $C_Graph =$this->AllocateColor($this->Layers[0],$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagefilledpolygon($this->Layers[0],$aPoints,4,$C_Graph); - } - - if ( $YIn[$klo] != "" && $YIn[$khi] != "" || $YLast == NULL ) - { - $PointsCount++; - $Points[] = $LayerWidth-$this->GAreaXOffset; - $Points[] = $YPos; - } - } - - $Points[] = $LayerWidth-$this->GAreaXOffset; - $Points[] = $LayerHeight; - - if ( !$AroundZero ) - { - $C_Graph =$this->AllocateColor($this->Layers[0],$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagefilledpolygon($this->Layers[0],$Points,$PointsCount,$C_Graph); - } - - imagecopymerge($this->Picture,$this->Layers[0],$this->GArea_X1,$this->GArea_Y1,0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[0]); - - $this->drawCubicCurve($Data,$DataDescription,$Accuracy,$ColName); - - $GraphID++; - } - } - - /* This function draw a filled line graph */ - function drawFilledLineGraph($Data,$DataDescription,$Alpha=100,$AroundZero=FALSE) - { - $Empty = -2147483647; - - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawFilledLineGraph",$DataDescription); - $this->validateData("drawFilledLineGraph",$Data); - - $LayerWidth = $this->GArea_X2-$this->GArea_X1; - $LayerHeight = $this->GArea_Y2-$this->GArea_Y1; - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $aPoints = ""; - $aPoints[] = $this->GAreaXOffset; - $aPoints[] = $LayerHeight; - - $this->Layers[0] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White = $this->AllocateColor($this->Layers[0],255,255,255); - imagefilledrectangle($this->Layers[0],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[0],$C_White); - - $XPos = $this->GAreaXOffset; - $XLast = -1; $PointsCount = 2; - $YZero = $LayerHeight - ((0-$this->VMin) * $this->DivisionRatio); - if ( $YZero > $LayerHeight ) { $YZero = $LayerHeight; } - - $YLast = $Empty; - foreach ( $Data as $Key => $Values ) - { - $Value = $Data[$Key][$ColName]; - $YPos = $LayerHeight - (($Value-$this->VMin) * $this->DivisionRatio); - - /* Save point into the image map if option activated */ - if ( $this->BuildMap ) - $this->addToImageMap($XPos-3,$YPos-3,$XPos+3,$YPos+3,$DataDescription["Description"][$ColName],$Data[$Key][$ColName].$DataDescription["Unit"]["Y"],"FLine"); - - if ( !is_numeric($Value) ) - { - $PointsCount++; - $aPoints[] = $XLast; - $aPoints[] = $LayerHeight; - - $YLast = $Empty; - } - else - { - $PointsCount++; - if ( $YLast <> $Empty ) - { $aPoints[] = $XPos; $aPoints[] = $YPos; } - else - { $PointsCount++; $aPoints[] = $XPos; $aPoints[] = $LayerHeight; $aPoints[] = $XPos; $aPoints[] = $YPos; } - - if ($YLast <> $Empty && $AroundZero) - { - $Points = ""; - $Points[] = $XLast; $Points[] = $YLast; - $Points[] = $XPos; - $Points[] = $YPos; - $Points[] = $XPos; - $Points[] = $YZero; - $Points[] = $XLast; - $Points[] = $YZero; - - $C_Graph = $this->AllocateColor($this->Layers[0],$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagefilledpolygon($this->Layers[0],$Points,4,$C_Graph); - } - $YLast = $YPos; - } - - $XLast = $XPos; - $XPos = $XPos + $this->DivisionWidth; - } - $aPoints[] = $LayerWidth - $this->GAreaXOffset; - $aPoints[] = $LayerHeight; - - if ( $AroundZero == FALSE ) - { - $C_Graph = $this->AllocateColor($this->Layers[0],$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagefilledpolygon($this->Layers[0],$aPoints,$PointsCount,$C_Graph); - } - - imagecopymerge($this->Picture,$this->Layers[0],$this->GArea_X1,$this->GArea_Y1,0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[0]); - $GraphID++; - $this->drawLineGraph($Data,$DataDescription,$ColName); - } - } - - /* This function draw a bar graph */ - function drawOverlayBarGraph($Data,$DataDescription,$Alpha=50) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawOverlayBarGraph",$DataDescription); - $this->validateData("drawOverlayBarGraph",$Data); - - $LayerWidth = $this->GArea_X2-$this->GArea_X1; - $LayerHeight = $this->GArea_Y2-$this->GArea_Y1; - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $this->Layers[$GraphID] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White = $this->AllocateColor($this->Layers[$GraphID],255,255,255); - $C_Graph = $this->AllocateColor($this->Layers[$GraphID],$this->Palette[$GraphID]["R"],$this->Palette[$GraphID]["G"],$this->Palette[$GraphID]["B"]); - imagefilledrectangle($this->Layers[$GraphID],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[$GraphID],$C_White); - - $XWidth = $this->DivisionWidth / 4; - $XPos = $this->GAreaXOffset; - $YZero = $LayerHeight - ((0-$this->VMin) * $this->DivisionRatio); - $XLast = -1; $PointsCount = 2; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName]) ) - { - $Value = $Data[$Key][$ColName]; - if ( is_numeric($Value) ) - { - $YPos = $LayerHeight - (($Value-$this->VMin) * $this->DivisionRatio); - - imagefilledrectangle($this->Layers[$GraphID],$XPos-$XWidth,$YPos,$XPos+$XWidth,$YZero,$C_Graph); - - $X1 = floor($XPos - $XWidth + $this->GArea_X1); $Y1 = floor($YPos+$this->GArea_Y1) + .2; - $X2 = floor($XPos + $XWidth + $this->GArea_X1); $Y2 = $this->GArea_Y2 - ((0-$this->VMin) * $this->DivisionRatio); - if ( $X1 <= $this->GArea_X1 ) { $X1 = $this->GArea_X1 + 1; } - if ( $X2 >= $this->GArea_X2 ) { $X2 = $this->GArea_X2 - 1; } - - /* Save point into the image map if option activated */ - if ( $this->BuildMap ) - $this->addToImageMap($X1,min($Y1,$Y2),$X2,max($Y1,$Y2),$DataDescription["Description"][$ColName],$Data[$Key][$ColName].$DataDescription["Unit"]["Y"],"oBar"); - - $this->drawLine($X1,$Y1,$X2,$Y1,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"],TRUE); - } - } - $XPos = $XPos + $this->DivisionWidth; - } - - $GraphID++; - } - - for($i=0;$i<=($GraphID-1);$i++) - { - imagecopymerge($this->Picture,$this->Layers[$i],$this->GArea_X1,$this->GArea_Y1,0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[$i]); - } - } - - /* This function draw a bar graph */ - function drawBarGraph($Data,$DataDescription,$Shadow=FALSE,$Alpha=100) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawBarGraph",$DataDescription); - $this->validateData("drawBarGraph",$Data); - - $GraphID = 0; - $Series = count($DataDescription["Values"]); - $SeriesWidth = $this->DivisionWidth / ($Series+1); - $SerieXOffset = $this->DivisionWidth / 2 - $SeriesWidth / 2; - - $YZero = $this->GArea_Y2 - ((0-$this->VMin) * $this->DivisionRatio); - if ( $YZero > $this->GArea_Y2 ) { $YZero = $this->GArea_Y2; } - - $SerieID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $XPos = $this->GArea_X1 + $this->GAreaXOffset - $SerieXOffset + $SeriesWidth * $SerieID; - $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - { - if ( is_numeric($Data[$Key][$ColName]) ) - { - $Value = $Data[$Key][$ColName]; - $YPos = $this->GArea_Y2 - (($Value-$this->VMin) * $this->DivisionRatio); - - /* Save point into the image map if option activated */ - if ( $this->BuildMap ) - { - $this->addToImageMap($XPos+1,min($YZero,$YPos),$XPos+$SeriesWidth-1,max($YZero,$YPos),$DataDescription["Description"][$ColName],$Data[$Key][$ColName].$DataDescription["Unit"]["Y"],"Bar"); - } - - if ( $Shadow && $Alpha == 100 ) - $this->drawRectangle($XPos+1,$YZero,$XPos+$SeriesWidth-1,$YPos,25,25,25,TRUE,$Alpha); - - $this->drawFilledRectangle($XPos+1,$YZero,$XPos+$SeriesWidth-1,$YPos,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"],TRUE,$Alpha); - } - } - $XPos = $XPos + $this->DivisionWidth; - } - $SerieID++; - } - } - - /* This function draw a stacked bar graph */ - function drawStackedBarGraph($Data,$DataDescription,$Alpha=50,$Contiguous=FALSE) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawBarGraph",$DataDescription); - $this->validateData("drawBarGraph",$Data); - - $GraphID = 0; - $Series = count($DataDescription["Values"]); - if ( $Contiguous ) - $SeriesWidth = $this->DivisionWidth; - else - $SeriesWidth = $this->DivisionWidth * .8; - - $YZero = $this->GArea_Y2 - ((0-$this->VMin) * $this->DivisionRatio); - if ( $YZero > $this->GArea_Y2 ) { $YZero = $this->GArea_Y2; } - - $SerieID = 0; $LastValue = ""; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $XPos = $this->GArea_X1 + $this->GAreaXOffset - $SeriesWidth / 2; - $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - { - if ( is_numeric($Data[$Key][$ColName]) ) - { - $Value = $Data[$Key][$ColName]; - - if ( isset($LastValue[$Key]) ) - { - $YPos = $this->GArea_Y2 - ((($Value+$LastValue[$Key])-$this->VMin) * $this->DivisionRatio); - $YBottom = $this->GArea_Y2 - (($LastValue[$Key]-$this->VMin) * $this->DivisionRatio); - $LastValue[$Key] += $Value; - } - else - { - $YPos = $this->GArea_Y2 - (($Value-$this->VMin) * $this->DivisionRatio); - $YBottom = $YZero; - $LastValue[$Key] = $Value; - } - - /* Save point into the image map if option activated */ - if ( $this->BuildMap ) - $this->addToImageMap($XPos+1,min($YBottom,$YPos),$XPos+$SeriesWidth-1,max($YBottom,$YPos),$DataDescription["Description"][$ColName],$Data[$Key][$ColName].$DataDescription["Unit"]["Y"],"sBar"); - - $this->drawFilledRectangle($XPos+1,$YBottom,$XPos+$SeriesWidth-1,$YPos,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"],TRUE,$Alpha); - } - } - $XPos = $XPos + $this->DivisionWidth; - } - $SerieID++; - } - } - - /* This function draw a limits bar graphs */ - function drawLimitsGraph($Data,$DataDescription,$R=0,$G=0,$B=0) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawLimitsGraph",$DataDescription); - $this->validateData("drawLimitsGraph",$Data); - - $XWidth = $this->DivisionWidth / 4; - $XPos = $this->GArea_X1 + $this->GAreaXOffset; - - foreach ( $Data as $Key => $Values ) - { - $Min = $Data[$Key][$DataDescription["Values"][0]]; - $Max = $Data[$Key][$DataDescription["Values"][0]]; - $GraphID = 0; $MaxID = 0; $MinID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if ( isset($Data[$Key][$ColName]) ) - { - if ( $Data[$Key][$ColName] > $Max && is_numeric($Data[$Key][$ColName])) - { $Max = $Data[$Key][$ColName]; $MaxID = $GraphID; } - } - if ( isset($Data[$Key][$ColName]) && is_numeric($Data[$Key][$ColName])) - { - if ( $Data[$Key][$ColName] < $Min ) - { $Min = $Data[$Key][$ColName]; $MinID = $GraphID; } - $GraphID++; - } - } - - $YPos = $this->GArea_Y2 - (($Max-$this->VMin) * $this->DivisionRatio); - $X1 = floor($XPos - $XWidth); $Y1 = floor($YPos) - .2; - $X2 = floor($XPos + $XWidth); - if ( $X1 <= $this->GArea_X1 ) { $X1 = $this->GArea_X1 + 1; } - if ( $X2 >= $this->GArea_X2 ) { $X2 = $this->GArea_X2 - 1; } - - $YPos = $this->GArea_Y2 - (($Min-$this->VMin) * $this->DivisionRatio); - $Y2 = floor($YPos) + .2; - - $this->drawLine(floor($XPos)-.2,$Y1+1,floor($XPos)-.2,$Y2-1,$R,$G,$B,TRUE); - $this->drawLine(floor($XPos)+.2,$Y1+1,floor($XPos)+.2,$Y2-1,$R,$G,$B,TRUE); - $this->drawLine($X1,$Y1,$X2,$Y1,$this->Palette[$MaxID]["R"],$this->Palette[$MaxID]["G"],$this->Palette[$MaxID]["B"],FALSE); - $this->drawLine($X1,$Y2,$X2,$Y2,$this->Palette[$MinID]["R"],$this->Palette[$MinID]["G"],$this->Palette[$MinID]["B"],FALSE); - - $XPos = $XPos + $this->DivisionWidth; - } - } - - /* This function draw radar axis centered on the graph area */ - function drawRadarAxis($Data,$DataDescription,$Mosaic=TRUE,$BorderOffset=10,$A_R=60,$A_G=60,$A_B=60,$S_R=200,$S_G=200,$S_B=200,$MaxValue=-1,$valueMod=1) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawRadarAxis",$DataDescription); - $this->validateData("drawRadarAxis",$Data); - - $C_TextColor = $this->AllocateColor($this->Picture,$A_R,$A_G,$A_B); - - /* Draw radar axis */ - $Points = count($Data); - $Radius = ( $this->GArea_Y2 - $this->GArea_Y1 ) / 2 - $BorderOffset; - $XCenter = ( $this->GArea_X2 - $this->GArea_X1 ) / 2 + $this->GArea_X1; - $YCenter = ( $this->GArea_Y2 - $this->GArea_Y1 ) / 2 + $this->GArea_Y1; - - /* Search for the max value */ - if ( $MaxValue == -1 ) - { - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - if ( $Data[$Key][$ColName] > $MaxValue ) { $MaxValue = $Data[$Key][$ColName]; } - } - } - } - - /* Draw the mosaic */ - if ( $Mosaic ) - { - $RadiusScale = $Radius / $MaxValue; - for ( $t=1; $t<=$MaxValue-1; $t++) - { - $TRadius = $RadiusScale * $t; - $LastX1 = -1; - - for ( $i=0; $i<=$Points; $i++) - { - $Angle = -90 + $i * 360/$Points; - $X1 = cos($Angle * 3.1418 / 180 ) * $TRadius + $XCenter; - $Y1 = sin($Angle * 3.1418 / 180 ) * $TRadius + $YCenter; - $X2 = cos($Angle * 3.1418 / 180 ) * ($TRadius+$RadiusScale) + $XCenter; - $Y2 = sin($Angle * 3.1418 / 180 ) * ($TRadius+$RadiusScale) + $YCenter; - - if ( $t % 2 == 1 && $LastX1 != -1) - { - $Plots = ""; - $Plots[] = $X1; $Plots[] = $Y1; - $Plots[] = $X2; $Plots[] = $Y2; - $Plots[] = $LastX2; $Plots[] = $LastY2; - $Plots[] = $LastX1; $Plots[] = $LastY1; - - $C_Graph = $this->AllocateColor($this->Picture,250,250,250); - imagefilledpolygon($this->Picture,$Plots,(count($Plots)+1)/2,$C_Graph); - } - - $LastX1 = $X1; $LastY1= $Y1; - $LastX2 = $X2; $LastY2= $Y2; - } - } - } - - - /* Draw the spider web */ - for ( $t=1; $t<=$MaxValue; $t++) - { - $TRadius = ( $Radius / $MaxValue ) * $t; - $LastX = -1; - - for ( $i=0; $i<=$Points; $i++) - { - $Angle = -90 + $i * 360/$Points; - $X = cos($Angle * 3.1418 / 180 ) * $TRadius + $XCenter; - $Y = sin($Angle * 3.1418 / 180 ) * $TRadius + $YCenter; - - if ( $LastX != -1 ) - $this->drawDottedLine($LastX,$LastY,$X,$Y,4,$S_R,$S_G,$S_B); - - $LastX = $X; $LastY= $Y; - } - } - - /* Draw the axis */ - for ( $i=0; $i<=$Points; $i++) - { - $Angle = -90 + $i * 360/$Points; - $X = cos($Angle * 3.1418 / 180 ) * $Radius + $XCenter; - $Y = sin($Angle * 3.1418 / 180 ) * $Radius + $YCenter; - - $this->drawLine($XCenter,$YCenter,$X,$Y,$A_R,$A_G,$A_B); - - $XOffset = 0; $YOffset = 0; - if (isset($Data[$i][$DataDescription["Position"]])) - { - $Label = $Data[$i][$DataDescription["Position"]]; - - $Positions = imagettfbbox($this->FontSize,0,$this->FontName,$Label); - $Width = $Positions[2] - $Positions[6]; - $Height = $Positions[3] - $Positions[7]; - - if ( $Angle >= 0 && $Angle <= 90 ) - $YOffset = $Height; - - if ( $Angle > 90 && $Angle <= 180 ) - { $YOffset = $Height; $XOffset = -$Width; } - - if ( $Angle > 180 && $Angle <= 270 ) - { $XOffset = -$Width; } - - imagettftext($this->Picture,$this->FontSize,0,$X+$XOffset,$Y+$YOffset,$C_TextColor,$this->FontName,$Label); - - if ( $this->BuildMap ) - { - $vecX = $X - $XCenter; - $vecY = $Y - $YCenter; - - // get a perpendicular vector - $vecXtemp = $vecX; - $vecX = -$vecY; - $vecY = $vecXtemp; - - // normalization - $vecLength = sqrt($vecX * $vecX + $vecY * $vecY); - $vecX = $vecX / $vecLength; - $vecY = $vecY / $vecLength; - - $tooltipValue = ''; - foreach ($DataDescription['Description'] as $key => $value) { - $tooltipValue .= $value.' : '.sprintf("%.2f", $Data[$i][$key]).';'; - } - - $offset = 10; - $poly = array( - array($X+$vecX*-$offset,$Y+$vecY*-$offset), - array($X+$vecX*+$offset,$Y+$vecY*+$offset), - array($XCenter+$vecX*+$offset,$YCenter+$vecY*+$offset), - array($XCenter+$vecX*-$offset,$YCenter+$vecY*-$offset), - ); - $this->addPolyToImageMap($poly,$Label,$tooltipValue,'Radar'); - } - } - } - - /* Write the values */ - for ( $t=1; $t<=$MaxValue; $t++) - { - if ($t % $valueMod != 0) - { continue; } - - $TRadius = ( $Radius / $MaxValue ) * $t; - - $Angle = -90 + 360 / $Points; - $X1 = $XCenter; - $Y1 = $YCenter - $TRadius; - $X2 = cos($Angle * 3.1418 / 180 ) * $TRadius + $XCenter; - $Y2 = sin($Angle * 3.1418 / 180 ) * $TRadius + $YCenter; - - $XPos = floor(($X2-$X1)/2) + $X1; - $YPos = floor(($Y2-$Y1)/2) + $Y1; - - $Positions = imagettfbbox($this->FontSize,0,$this->FontName,$t); - $X = $XPos - ( $X+$Positions[2] - $X+$Positions[6] ) / 2; - $Y = $YPos + $this->FontSize; - - $this->drawFilledRoundedRectangle($X+$Positions[6]-2,$Y+$Positions[7]-1,$X+$Positions[2]+4,$Y+$Positions[3]+1,2,240,240,240); - $this->drawRoundedRectangle($X+$Positions[6]-2,$Y+$Positions[7]-1,$X+$Positions[2]+4,$Y+$Positions[3]+1,2,220,220,220); - imagettftext($this->Picture,$this->FontSize,0,$X,$Y,$C_TextColor,$this->FontName,$t); - } - } - - /* This function draw a radar graph centered on the graph area */ - function drawRadar($Data,$DataDescription,$BorderOffset=10,$MaxValue=-1) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawRadar",$DataDescription); - $this->validateData("drawRadar",$Data); - - $Points = count($Data); - $Radius = ( $this->GArea_Y2 - $this->GArea_Y1 ) / 2 - $BorderOffset; - $XCenter = ( $this->GArea_X2 - $this->GArea_X1 ) / 2 + $this->GArea_X1; - $YCenter = ( $this->GArea_Y2 - $this->GArea_Y1 ) / 2 + $this->GArea_Y1; - - /* Search for the max value */ - if ( $MaxValue == -1 ) - { - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - if ( $Data[$Key][$ColName] > $MaxValue ) { $MaxValue = $Data[$Key][$ColName]; } - } - } - } - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $Angle = -90; - $XLast = -1; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - { - $Value = $Data[$Key][$ColName]; - $Strength = ( $Radius / $MaxValue ) * $Value; - - $XPos = cos($Angle * 3.1418 / 180 ) * $Strength + $XCenter; - $YPos = sin($Angle * 3.1418 / 180 ) * $Strength + $YCenter; - - if ( $XLast != -1 ) - $this->drawLine($XLast,$YLast,$XPos,$YPos,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - - if ( $XLast == -1 ) - { $FirstX = $XPos; $FirstY = $YPos; } - - $Angle = $Angle + (360/$Points); - $XLast = $XPos; - $YLast = $YPos; - } - } - $this->drawLine($XPos,$YPos,$FirstX,$FirstY,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - $GraphID++; - } - } - - /* This function draw a radar graph centered on the graph area */ - function drawFilledRadar($Data,$DataDescription,$Alpha=50,$BorderOffset=10,$MaxValue=-1) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawFilledRadar",$DataDescription); - $this->validateData("drawFilledRadar",$Data); - - $Points = count($Data); - $LayerWidth = $this->GArea_X2-$this->GArea_X1; - $LayerHeight = $this->GArea_Y2-$this->GArea_Y1; - $Radius = ( $this->GArea_Y2 - $this->GArea_Y1 ) / 2 - $BorderOffset; - $XCenter = ( $this->GArea_X2 - $this->GArea_X1 ) / 2; - $YCenter = ( $this->GArea_Y2 - $this->GArea_Y1 ) / 2; - - /* Search for the max value */ - if ( $MaxValue == -1 ) - { - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - if ( $Data[$Key][$ColName] > $MaxValue && is_numeric($Data[$Key][$ColName])) { $MaxValue = $Data[$Key][$ColName]; } - } - } - } - - $GraphID = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - $ID = 0; - foreach ( $DataDescription["Description"] as $keyI => $ValueI ) - { if ( $keyI == $ColName ) { $ColorID = $ID; }; $ID++; } - - $Angle = -90; - $XLast = -1; - $Plots = ""; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - { - $Value = $Data[$Key][$ColName]; - if ( !is_numeric($Value) ) { $Value = 0; } - $Strength = ( $Radius / $MaxValue ) * $Value; - - $XPos = cos($Angle * 3.1418 / 180 ) * $Strength + $XCenter; - $YPos = sin($Angle * 3.1418 / 180 ) * $Strength + $YCenter; - - $Plots[] = $XPos; - $Plots[] = $YPos; - - $Angle = $Angle + (360/$Points); - $XLast = $XPos; - $YLast = $YPos; - } - } - - if (isset($Plots[0])) - { - $Plots[] = $Plots[0]; - $Plots[] = $Plots[1]; - - $this->Layers[0] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White = $this->AllocateColor($this->Layers[0],255,255,255); - imagefilledrectangle($this->Layers[0],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[0],$C_White); - - $C_Graph = $this->AllocateColor($this->Layers[0],$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - imagefilledpolygon($this->Layers[0],$Plots,(count($Plots)+1)/2,$C_Graph); - - imagecopymerge($this->Picture,$this->Layers[0],$this->GArea_X1,$this->GArea_Y1,0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[0]); - - for($i=0;$i<=count($Plots)-4;$i=$i+2) - $this->drawLine($Plots[$i]+$this->GArea_X1,$Plots[$i+1]+$this->GArea_Y1,$Plots[$i+2]+$this->GArea_X1,$Plots[$i+3]+$this->GArea_Y1,$this->Palette[$ColorID]["R"],$this->Palette[$ColorID]["G"],$this->Palette[$ColorID]["B"]); - } - - $GraphID++; - } - } - - /* This function draw a flat pie chart */ - function drawBasicPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$R=255,$G=255,$B=255,$Decimals=0) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawBasicPieGraph",$DataDescription,FALSE); - $this->validateData("drawBasicPieGraph",$Data); - - /* Determine pie sum */ - $Series = 0; $PieSum = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if ( $ColName != $DataDescription["Position"] ) - { - $Series++; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - $PieSum = $PieSum + $Data[$Key][$ColName]; $iValues[] = $Data[$Key][$ColName]; $iLabels[] = $Data[$Key][$DataDescription["Position"]]; - } - } - } - - /* Validate serie */ - if ( $Series != 1 ) - RaiseFatal("Pie chart can only accept one serie of data."); - - $SpliceRatio = 360 / $PieSum; - $SplicePercent = 100 / $PieSum; - - /* Calculate all polygons */ - $Angle = 0; $TopPlots = ""; - foreach($iValues as $Key => $Value) - { - $TopPlots[$Key][] = $XPos; - $TopPlots[$Key][] = $YPos; - - /* Process labels position & size */ - $Caption = ""; - if ( !($DrawLabels == PIE_NOLABEL) ) - { - $TAngle = $Angle+($Value*$SpliceRatio/2); - if ($DrawLabels == PIE_PERCENTAGE) - $Caption = (round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - elseif ($DrawLabels == PIE_LABELS) - $Caption = $iLabels[$Key]; - elseif ($DrawLabels == PIE_PERCENTAGE_LABEL) - $Caption = $iLabels[$Key]."\r\n".(round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - elseif ($DrawLabels == PIE_PERCENTAGE_LABEL) - $Caption = $iLabels[$Key]."\r\n".(round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Caption); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = abs($Position[1])+abs($Position[3]); - - $TX = cos(($TAngle) * 3.1418 / 180 ) * ($Radius+10) + $XPos; - - if ( $TAngle > 0 && $TAngle < 180 ) - $TY = sin(($TAngle) * 3.1418 / 180 ) * ($Radius+10) + $YPos + 4; - else - $TY = sin(($TAngle) * 3.1418 / 180 ) * ($Radius+4) + $YPos - ($TextHeight/2); - - if ( $TAngle > 90 && $TAngle < 270 ) - $TX = $TX - $TextWidth; - - $C_TextColor = $this->AllocateColor($this->Picture,70,70,70); - imagettftext($this->Picture,$this->FontSize,0,$TX,$TY,$C_TextColor,$this->FontName,$Caption); - } - - /* Process pie slices */ - for($iAngle=$Angle;$iAngle<=$Angle+$Value*$SpliceRatio;$iAngle=$iAngle+.5) - { - $TopX = cos($iAngle * 3.1418 / 180 ) * $Radius + $XPos; - $TopY = sin($iAngle * 3.1418 / 180 ) * $Radius + $YPos; - - $TopPlots[$Key][] = $TopX; - $TopPlots[$Key][] = $TopY; - } - - $TopPlots[$Key][] = $XPos; - $TopPlots[$Key][] = $YPos; - - $Angle = $iAngle; - } - $PolyPlots = $TopPlots; - - /* Set array values type to float --- PHP Bug with imagefilledpolygon casting to integer */ - foreach ($TopPlots as $Key => $Value) - { foreach ($TopPlots[$Key] as $Key2 => $Value2) { settype($TopPlots[$Key][$Key2],"float"); } } - - /* Draw Top polygons */ - foreach ($PolyPlots as $Key => $Value) - { - $C_GraphLo = $this->AllocateColor($this->Picture,$this->Palette[$Key]["R"],$this->Palette[$Key]["G"],$this->Palette[$Key]["B"]); - imagefilledpolygon($this->Picture,$PolyPlots[$Key],(count($PolyPlots[$Key])+1)/2,$C_GraphLo); - } - - $this->drawCircle($XPos-.5,$YPos-.5,$Radius,$R,$G,$B); - $this->drawCircle($XPos-.5,$YPos-.5,$Radius+.5,$R,$G,$B); - - /* Draw Top polygons */ - foreach ($TopPlots as $Key => $Value) - { - for($j=0;$j<=count($TopPlots[$Key])-4;$j=$j+2) - $this->drawLine($TopPlots[$Key][$j],$TopPlots[$Key][$j+1],$TopPlots[$Key][$j+2],$TopPlots[$Key][$j+3],$R,$G,$B); - } - } - - function drawFlatPieGraphWithShadow($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$SpliceDistance=0,$Decimals=0) - { - $this->drawFlatPieGraph($Data,$DataDescription,$XPos+$this->ShadowXDistance,$YPos+$this->ShadowYDistance,$Radius,PIE_NOLABEL,$SpliceDistance,$Decimals,TRUE); - $this->drawFlatPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius,$DrawLabels,$SpliceDistance,$Decimals,FALSE); - } - - /* This function draw a flat pie chart */ - function drawFlatPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$SpliceDistance=0,$Decimals=0,$AllBlack=FALSE) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawFlatPieGraph",$DataDescription,FALSE); - $this->validateData("drawFlatPieGraph",$Data); - - $ShadowStatus = $this->ShadowActive ; $this->ShadowActive = FALSE; - - /* Determine pie sum */ - $Series = 0; $PieSum = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if ( $ColName != $DataDescription["Position"] ) - { - $Series++; - foreach ( $Data as $Key => $Values ) - { - if ( isset($Data[$Key][$ColName])) - $PieSum = $PieSum + $Data[$Key][$ColName]; $iValues[] = $Data[$Key][$ColName]; $iLabels[] = $Data[$Key][$DataDescription["Position"]]; - } - } - } - - /* Validate serie */ - if ( $Series != 1 ) - { - RaiseFatal("Pie chart can only accept one serie of data."); - return(0); - } - - $SpliceRatio = 360 / $PieSum; - $SplicePercent = 100 / $PieSum; - - /* Calculate all polygons */ - $Angle = 0; $TopPlots = ""; - foreach($iValues as $Key => $Value) - { - $XOffset = cos(($Angle+($Value/2*$SpliceRatio)) * 3.1418 / 180 ) * $SpliceDistance; - $YOffset = sin(($Angle+($Value/2*$SpliceRatio)) * 3.1418 / 180 ) * $SpliceDistance; - - $TopPlots[$Key][] = round($XPos + $XOffset); - $TopPlots[$Key][] = round($YPos + $YOffset); - - if ( $AllBlack ) - { $Rc = $this->ShadowRColor; $Gc = $this->ShadowGColor; $Bc = $this->ShadowBColor; } - else - { $Rc = $this->Palette[$Key]["R"]; $Gc = $this->Palette[$Key]["G"]; $Bc = $this->Palette[$Key]["B"]; } - - $XLineLast = ""; $YLineLast = ""; - - /* Process labels position & size */ - $Caption = ""; - if ( !($DrawLabels == PIE_NOLABEL) ) - { - $TAngle = $Angle+($Value*$SpliceRatio/2); - if ($DrawLabels == PIE_PERCENTAGE) - $Caption = (round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - elseif ($DrawLabels == PIE_LABELS) - $Caption = $iLabels[$Key]; - elseif ($DrawLabels == PIE_PERCENTAGE_LABEL) - $Caption = $iLabels[$Key]."\r\n".(round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - elseif ($DrawLabels == PIE_PERCENTAGE_LABEL) - $Caption = $iLabels[$Key]."\r\n".(round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Caption); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = abs($Position[1])+abs($Position[3]); - - $TX = cos(($TAngle) * 3.1418 / 180 ) * ($Radius+10+$SpliceDistance) + $XPos; - - if ( $TAngle > 0 && $TAngle < 180 ) - $TY = sin(($TAngle) * 3.1418 / 180 ) * ($Radius+10+$SpliceDistance) + $YPos + 4; - else - $TY = sin(($TAngle) * 3.1418 / 180 ) * ($Radius+$SpliceDistance+4) + $YPos - ($TextHeight/2); - - if ( $TAngle > 90 && $TAngle < 270 ) - $TX = $TX - $TextWidth; - - $C_TextColor = $this->AllocateColor($this->Picture,70,70,70); - imagettftext($this->Picture,$this->FontSize,0,$TX,$TY,$C_TextColor,$this->FontName,$Caption); - } - - /* Process pie slices */ - if ( !$AllBlack ) - $LineColor = $this->AllocateColor($this->Picture,$Rc,$Gc,$Bc); - else - $LineColor = $this->AllocateColor($this->Picture,$Rc,$Gc,$Bc); - - $XLineLast = ""; $YLineLast = ""; - for($iAngle=$Angle;$iAngle<=$Angle+$Value*$SpliceRatio;$iAngle=$iAngle+.5) - { - $PosX = cos($iAngle * 3.1418 / 180 ) * $Radius + $XPos + $XOffset; - $PosY = sin($iAngle * 3.1418 / 180 ) * $Radius + $YPos + $YOffset; - - $TopPlots[$Key][] = round($PosX); $TopPlots[$Key][] = round($PosY); - - if ( $iAngle == $Angle || $iAngle == $Angle+$Value*$SpliceRatio || $iAngle +.5 > $Angle+$Value*$SpliceRatio) - $this->drawLine($XPos+$XOffset,$YPos+$YOffset,$PosX,$PosY,$Rc,$Gc,$Bc); - - if ( $XLineLast != "" ) - $this->drawLine($XLineLast,$YLineLast,$PosX,$PosY,$Rc,$Gc,$Bc); - - $XLineLast = $PosX; $YLineLast = $PosY; - } - - $TopPlots[$Key][] = round($XPos + $XOffset); $TopPlots[$Key][] = round($YPos + $YOffset); - - $Angle = $iAngle; - } - $PolyPlots = $TopPlots; - - /* Draw Top polygons */ - foreach ($PolyPlots as $Key => $Value) - { - if ( !$AllBlack ) - $C_GraphLo = $this->AllocateColor($this->Picture,$this->Palette[$Key]["R"],$this->Palette[$Key]["G"],$this->Palette[$Key]["B"]); - else - $C_GraphLo = $this->AllocateColor($this->Picture,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor); - - imagefilledpolygon($this->Picture,$PolyPlots[$Key],(count($PolyPlots[$Key])+1)/2,$C_GraphLo); - } - $this->ShadowActive = $ShadowStatus; - } - - /* This function draw a pseudo-3D pie chart */ - function drawPieGraph($Data,$DataDescription,$XPos,$YPos,$Radius=100,$DrawLabels=PIE_NOLABEL,$EnhanceColors=TRUE,$Skew=60,$SpliceHeight=20,$SpliceDistance=0,$Decimals=0) - { - /* Validate the Data and DataDescription array */ - $this->validateDataDescription("drawPieGraph",$DataDescription,FALSE); - $this->validateData("drawPieGraph",$Data); - - /* Determine pie sum */ - $Series = 0; $PieSum = 0; $rPieSum = 0; - foreach ( $DataDescription["Values"] as $Key2 => $ColName ) - { - if ( $ColName != $DataDescription["Position"] ) - { - $Series++; - foreach ( $Data as $Key => $Values ) - if ( isset($Data[$Key][$ColName])) - { - if ( $Data[$Key][$ColName] == 0 ) - { $iValues[] = 0; $rValues[] = 0; $iLabels[] = $Data[$Key][$DataDescription["Position"]]; } - // Removed : $PieSum++; $rValues[] = 1; - else - { $PieSum += $Data[$Key][$ColName]; $iValues[] = $Data[$Key][$ColName]; $iLabels[] = $Data[$Key][$DataDescription["Position"]]; $rValues[] = $Data[$Key][$ColName]; $rPieSum += $Data[$Key][$ColName];} - } - } - } - - /* Validate serie */ - if ( $Series != 1 ) - RaiseFatal("Pie chart can only accept one serie of data."); - - $SpliceDistanceRatio = $SpliceDistance; - $SkewHeight = ($Radius * $Skew) / 100; - $SpliceRatio = (360 - $SpliceDistanceRatio * count($iValues) ) / $PieSum; - $SplicePercent = 100 / $PieSum; - $rSplicePercent = 100 / $rPieSum; - - /* Calculate all polygons */ - $Angle = 0; $CDev = 5; - $TopPlots = ""; $BotPlots = ""; - $aTopPlots = ""; $aBotPlots = ""; - foreach($iValues as $Key => $Value) - { - $XCenterPos = cos(($Angle-$CDev+($Value*$SpliceRatio+$SpliceDistanceRatio)/2) * 3.1418 / 180 ) * $SpliceDistance + $XPos; - $YCenterPos = sin(($Angle-$CDev+($Value*$SpliceRatio+$SpliceDistanceRatio)/2) * 3.1418 / 180 ) * $SpliceDistance + $YPos; - $XCenterPos2 = cos(($Angle+$CDev+($Value*$SpliceRatio+$SpliceDistanceRatio)/2) * 3.1418 / 180 ) * $SpliceDistance + $XPos; - $YCenterPos2 = sin(($Angle+$CDev+($Value*$SpliceRatio+$SpliceDistanceRatio)/2) * 3.1418 / 180 ) * $SpliceDistance + $YPos; - - $TopPlots[$Key][] = round($XCenterPos); $BotPlots[$Key][] = round($XCenterPos); - $TopPlots[$Key][] = round($YCenterPos); $BotPlots[$Key][] = round($YCenterPos + $SpliceHeight); - $aTopPlots[$Key][] = $XCenterPos; $aBotPlots[$Key][] = $XCenterPos; - $aTopPlots[$Key][] = $YCenterPos; $aBotPlots[$Key][] = $YCenterPos + $SpliceHeight; - - /* Process labels position & size */ - $Caption = ""; - if ( !($DrawLabels == PIE_NOLABEL) ) - { - $TAngle = $Angle+($Value*$SpliceRatio/2); - if ($DrawLabels == PIE_PERCENTAGE) - $Caption = (round($rValues[$Key] * pow(10,$Decimals) * $rSplicePercent)/pow(10,$Decimals))."%"; - elseif ($DrawLabels == PIE_LABELS) - $Caption = $iLabels[$Key]; - elseif ($DrawLabels == PIE_PERCENTAGE_LABEL) - $Caption = $iLabels[$Key]."\r\n".(round($Value * pow(10,$Decimals) * $SplicePercent)/pow(10,$Decimals))."%"; - - $Position = imageftbbox($this->FontSize,0,$this->FontName,$Caption); - $TextWidth = $Position[2]-$Position[0]; - $TextHeight = abs($Position[1])+abs($Position[3]); - - $TX = cos(($TAngle) * 3.1418 / 180 ) * ($Radius + 10)+ $XPos; - - if ( $TAngle > 0 && $TAngle < 180 ) - $TY = sin(($TAngle) * 3.1418 / 180 ) * ($SkewHeight + 10) + $YPos + $SpliceHeight + 4; - else - $TY = sin(($TAngle) * 3.1418 / 180 ) * ($SkewHeight + 4) + $YPos - ($TextHeight/2); - - if ( $TAngle > 90 && $TAngle < 270 ) - $TX = $TX - $TextWidth; - - $C_TextColor = $this->AllocateColor($this->Picture,70,70,70); - imagettftext($this->Picture,$this->FontSize,0,$TX,$TY,$C_TextColor,$this->FontName,$Caption); - } - - /* Process pie slices */ - for($iAngle=$Angle;$iAngle<=$Angle+$Value*$SpliceRatio;$iAngle=$iAngle+.5) - { - $TopX = cos($iAngle * 3.1418 / 180 ) * $Radius + $XPos; - $TopY = sin($iAngle * 3.1418 / 180 ) * $SkewHeight + $YPos; - - $TopPlots[$Key][] = round($TopX); $BotPlots[$Key][] = round($TopX); - $TopPlots[$Key][] = round($TopY); $BotPlots[$Key][] = round($TopY + $SpliceHeight); - $aTopPlots[$Key][] = $TopX; $aBotPlots[$Key][] = $TopX; - $aTopPlots[$Key][] = $TopY; $aBotPlots[$Key][] = $TopY + $SpliceHeight; - } - - $TopPlots[$Key][] = round($XCenterPos2); $BotPlots[$Key][] = round($XCenterPos2); - $TopPlots[$Key][] = round($YCenterPos2); $BotPlots[$Key][] = round($YCenterPos2 + $SpliceHeight); - $aTopPlots[$Key][] = $XCenterPos2; $aBotPlots[$Key][] = $XCenterPos2; - $aTopPlots[$Key][] = $YCenterPos2; $aBotPlots[$Key][] = $YCenterPos2 + $SpliceHeight; - - $Angle = $iAngle + $SpliceDistanceRatio; - } - - /* Draw Bottom polygons */ - foreach($iValues as $Key => $Value) - { - $C_GraphLo = $this->AllocateColor($this->Picture,$this->Palette[$Key]["R"],$this->Palette[$Key]["G"],$this->Palette[$Key]["B"],-20); - imagefilledpolygon($this->Picture,$BotPlots[$Key],(count($BotPlots[$Key])+1)/2,$C_GraphLo); - - if ( $EnhanceColors ) { $En = -10; } else { $En = 0; } - - for($j=0;$j<=count($aBotPlots[$Key])-4;$j=$j+2) - $this->drawLine($aBotPlots[$Key][$j],$aBotPlots[$Key][$j+1],$aBotPlots[$Key][$j+2],$aBotPlots[$Key][$j+3],$this->Palette[$Key]["R"]+$En,$this->Palette[$Key]["G"]+$En,$this->Palette[$Key]["B"]+$En); - } - - /* Draw pie layers */ - if ( $EnhanceColors ) { $ColorRatio = 30 / $SpliceHeight; } else { $ColorRatio = 25 / $SpliceHeight; } - for($i=$SpliceHeight-1;$i>=1;$i--) - { - foreach($iValues as $Key => $Value) - { - $C_GraphLo = $this->AllocateColor($this->Picture,$this->Palette[$Key]["R"],$this->Palette[$Key]["G"],$this->Palette[$Key]["B"],-10); - $Plots = ""; $Plot = 0; - foreach($TopPlots[$Key] as $Key2 => $Value2) - { - $Plot++; - if ( $Plot % 2 == 1 ) - $Plots[] = $Value2; - else - $Plots[] = $Value2+$i; - } - imagefilledpolygon($this->Picture,$Plots,(count($Plots)+1)/2,$C_GraphLo); - - $Index = count($Plots); - if ($EnhanceColors ) {$ColorFactor = -20 + ($SpliceHeight - $i) * $ColorRatio; } else { $ColorFactor = 0; } - - $this->drawAntialiasPixel($Plots[0],$Plots[1],$this->Palette[$Key]["R"]+$ColorFactor,$this->Palette[$Key]["G"]+$ColorFactor,$this->Palette[$Key]["B"]+$ColorFactor); - $this->drawAntialiasPixel($Plots[2],$Plots[3],$this->Palette[$Key]["R"]+$ColorFactor,$this->Palette[$Key]["G"]+$ColorFactor,$this->Palette[$Key]["B"]+$ColorFactor); - $this->drawAntialiasPixel($Plots[$Index-4],$Plots[$Index-3],$this->Palette[$Key]["R"]+$ColorFactor,$this->Palette[$Key]["G"]+$ColorFactor,$this->Palette[$Key]["B"]+$ColorFactor); - } - } - - if ( $this->BuildMap ) - { - // Add points to Image Map. - foreach ($TopPlots as $key => $PointArr) - { - $serieName = $Data[$key][$DataDescription['Values'][1]]; - $serieValue = $Data[$key][$DataDescription['Values'][0]]; - - // last point of the arc - $lastX = $PointArr[count($PointArr)-4]; - $lastY = $PointArr[count($PointArr)-3]; - - // the point at the middle - $middleX = $PointArr[0]; - $middleY = $PointArr[1]; - - // first point in the arc - $firstX = $PointArr[2]; - $firstY = $PointArr[3]; - - // point on the first third of the arc - $firstThird = count($PointArr)/3; - $firstThirdX = $PointArr[$firstThird + ($firstThird % 2)]; - $firstThirdY = $PointArr[$firstThird + ($firstThird % 2)+1]; - - // point on the second third of the arc - $secondThird = count($PointArr)/3*2; - $secondThirdX = $PointArr[$secondThird + ($secondThird % 2)]; - $secondThirdY = $PointArr[$secondThird + ($secondThird % 2)+1]; - - // Will create three polygons for every piece of the pie. In such way - // no polygon will be concave. JS only works with convex polygons. - $poly = array( - array($middleX,$middleY), - array($firstX,$firstY), - array($firstThirdX,$firstThirdY), - ); - $this->addPolyToImageMap($poly,$serieName,$serieValue,"Pie"); - - $poly = array( - array($middleX,$middleY), - array($firstThirdX,$firstThirdY), - array($secondThirdX,$secondThirdY), - ); - $this->addPolyToImageMap($poly,$serieName,$serieValue,"Pie"); - - $poly = array( - array($middleX,$middleY), - array($secondThirdX,$secondThirdY), - array($lastX,$lastY), - ); - $this->addPolyToImageMap($poly,$serieName,$serieValue,"Pie"); - } - } - - /* Draw Top polygons */ - for($Key=count($iValues)-1;$Key>=0;$Key--) - { - $C_GraphLo = $this->AllocateColor($this->Picture,$this->Palette[$Key]["R"],$this->Palette[$Key]["G"],$this->Palette[$Key]["B"]); - imagefilledpolygon($this->Picture,$TopPlots[$Key],(count($TopPlots[$Key])+1)/2,$C_GraphLo); - - if ( $EnhanceColors ) { $En = 10; } else { $En = 0; } - for($j=0;$j<=count($aTopPlots[$Key])-4;$j=$j+2) - $this->drawLine($aTopPlots[$Key][$j],$aTopPlots[$Key][$j+1],$aTopPlots[$Key][$j+2],$aTopPlots[$Key][$j+3],$this->Palette[$Key]["R"]+$En,$this->Palette[$Key]["G"]+$En,$this->Palette[$Key]["B"]+$En); - } - } - - /* This function can be used to set the background color */ - function drawBackground($R,$G,$B) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_Background = $this->AllocateColor($this->Picture,$R,$G,$B); - imagefilledrectangle($this->Picture,0,0,$this->XSize,$this->YSize,$C_Background); - } - - /* This function can be used to set the background color */ - function drawGraphAreaGradient($R,$G,$B,$Decay,$Target=TARGET_GRAPHAREA) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - if ( $Target == TARGET_GRAPHAREA ) { $X1 = $this->GArea_X1+1; $X2 = $this->GArea_X2-1; $Y1 = $this->GArea_Y1+1; $Y2 = $this->GArea_Y2; } - if ( $Target == TARGET_BACKGROUND ) { $X1 = 0; $X2 = $this->XSize; $Y1 = 0; $Y2 = $this->YSize; } - - /* Positive gradient */ - if ( $Decay > 0 ) - { - $YStep = ($Y2 - $Y1 - 2) / $Decay; - for($i=0;$i<=$Decay;$i++) - { - $R-=1;$G-=1;$B-=1; - $Yi1 = $Y1 + ( $i * $YStep ); - $Yi2 = ceil( $Yi1 + ( $i * $YStep ) + $YStep ); - if ( $Yi2 >= $Yi2 ) { $Yi2 = $Y2-1; } - - $C_Background = $this->AllocateColor($this->Picture,$R,$G,$B); - imagefilledrectangle($this->Picture,$X1,$Yi1,$X2,$Yi2,$C_Background); - } - } - - /* Negative gradient */ - if ( $Decay < 0 ) - { - $YStep = ($Y2 - $Y1 - 2) / -$Decay; - $Yi1 = $Y1; $Yi2 = $Y1+$YStep; - for($i=-$Decay;$i>=0;$i--) - { - $R+=1;$G+=1;$B+=1; - $C_Background = $this->AllocateColor($this->Picture,$R,$G,$B); - imagefilledrectangle($this->Picture,$X1,$Yi1,$X2,$Yi2,$C_Background); - - $Yi1+= $YStep; - $Yi2+= $YStep; - if ( $Yi2 >= $Yi2 ) { $Yi2 = $Y2-1; } - } - } - } - - /* This function create a rectangle with antialias */ - function drawRectangle($X1,$Y1,$X2,$Y2,$R,$G,$B) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_Rectangle = $this->AllocateColor($this->Picture,$R,$G,$B); - - $X1=$X1-.2;$Y1=$Y1-.2; - $X2=$X2+.2;$Y2=$Y2+.2; - $this->drawLine($X1,$Y1,$X2,$Y1,$R,$G,$B); - $this->drawLine($X2,$Y1,$X2,$Y2,$R,$G,$B); - $this->drawLine($X2,$Y2,$X1,$Y2,$R,$G,$B); - $this->drawLine($X1,$Y2,$X1,$Y1,$R,$G,$B); - } - - /* This function create a filled rectangle with antialias */ - function drawFilledRectangle($X1,$Y1,$X2,$Y2,$R,$G,$B,$DrawBorder=TRUE,$Alpha=100,$NoFallBack=FALSE) - { - if ( $X2 < $X1 ) { list($X1, $X2) = array($X2, $X1); } - if ( $Y2 < $Y1 ) { list($Y1, $Y2) = array($Y2, $Y1); } - - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - if ( $Alpha == 100 ) - { - /* Process shadows */ - if ( $this->ShadowActive && !$NoFallBack ) - { - $this->drawFilledRectangle($X1+$this->ShadowXDistance,$Y1+$this->ShadowYDistance,$X2+$this->ShadowXDistance,$Y2+$this->ShadowYDistance,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor,FALSE,$this->ShadowAlpha,TRUE); - if ( $this->ShadowBlur != 0 ) - { - $AlphaDecay = ($this->ShadowAlpha / $this->ShadowBlur); - - for($i=1; $i<=$this->ShadowBlur; $i++) - $this->drawFilledRectangle($X1+$this->ShadowXDistance-$i/2,$Y1+$this->ShadowYDistance-$i/2,$X2+$this->ShadowXDistance-$i/2,$Y2+$this->ShadowYDistance-$i/2,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor,FALSE,$this->ShadowAlpha-$AlphaDecay*$i,TRUE); - for($i=1; $i<=$this->ShadowBlur; $i++) - $this->drawFilledRectangle($X1+$this->ShadowXDistance+$i/2,$Y1+$this->ShadowYDistance+$i/2,$X2+$this->ShadowXDistance+$i/2,$Y2+$this->ShadowYDistance+$i/2,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor,FALSE,$this->ShadowAlpha-$AlphaDecay*$i,TRUE); - } - } - - $C_Rectangle = $this->AllocateColor($this->Picture,$R,$G,$B); - imagefilledrectangle($this->Picture,round($X1),round($Y1),round($X2),round($Y2),$C_Rectangle); - } - else - { - $LayerWidth = abs($X2-$X1)+2; - $LayerHeight = abs($Y2-$Y1)+2; - - $this->Layers[0] = imagecreatetruecolor($LayerWidth,$LayerHeight); - $C_White = $this->AllocateColor($this->Layers[0],255,255,255); - imagefilledrectangle($this->Layers[0],0,0,$LayerWidth,$LayerHeight,$C_White); - imagecolortransparent($this->Layers[0],$C_White); - - $C_Rectangle = $this->AllocateColor($this->Layers[0],$R,$G,$B); - imagefilledrectangle($this->Layers[0],round(1),round(1),round($LayerWidth-1),round($LayerHeight-1),$C_Rectangle); - - imagecopymerge($this->Picture,$this->Layers[0],round(min($X1,$X2)-1),round(min($Y1,$Y2)-1),0,0,$LayerWidth,$LayerHeight,$Alpha); - imagedestroy($this->Layers[0]); - } - - if ( $DrawBorder ) - { - $ShadowSettings = $this->ShadowActive; $this->ShadowActive = FALSE; - $this->drawRectangle($X1,$Y1,$X2,$Y2,$R,$G,$B); - $this->ShadowActive = $ShadowSettings; - } - } - - /* This function create a rectangle with rounded corners and antialias */ - function drawRoundedRectangle($X1,$Y1,$X2,$Y2,$Radius,$R,$G,$B) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_Rectangle = $this->AllocateColor($this->Picture,$R,$G,$B); - - $Step = 90 / ((3.1418 * $Radius)/2); - - for($i=0;$i<=90;$i=$i+$Step) - { - $X = cos(($i+180)*3.1418/180) * $Radius + $X1 + $Radius; - $Y = sin(($i+180)*3.1418/180) * $Radius + $Y1 + $Radius; - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - - $X = cos(($i-90)*3.1418/180) * $Radius + $X2 - $Radius; - $Y = sin(($i-90)*3.1418/180) * $Radius + $Y1 + $Radius; - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - - $X = cos(($i)*3.1418/180) * $Radius + $X2 - $Radius; - $Y = sin(($i)*3.1418/180) * $Radius + $Y2 - $Radius; - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - - $X = cos(($i+90)*3.1418/180) * $Radius + $X1 + $Radius; - $Y = sin(($i+90)*3.1418/180) * $Radius + $Y2 - $Radius; - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - } - - $X1=$X1-.2;$Y1=$Y1-.2; - $X2=$X2+.2;$Y2=$Y2+.2; - $this->drawLine($X1+$Radius,$Y1,$X2-$Radius,$Y1,$R,$G,$B); - $this->drawLine($X2,$Y1+$Radius,$X2,$Y2-$Radius,$R,$G,$B); - $this->drawLine($X2-$Radius,$Y2,$X1+$Radius,$Y2,$R,$G,$B); - $this->drawLine($X1,$Y2-$Radius,$X1,$Y1+$Radius,$R,$G,$B); - } - - /* This function create a filled rectangle with rounded corners and antialias */ - function drawFilledRoundedRectangle($X1,$Y1,$X2,$Y2,$Radius,$R,$G,$B) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_Rectangle = $this->AllocateColor($this->Picture,$R,$G,$B); - - $Step = 90 / ((3.1418 * $Radius)/2); - - for($i=0;$i<=90;$i=$i+$Step) - { - $Xi1 = cos(($i+180)*3.1418/180) * $Radius + $X1 + $Radius; - $Yi1 = sin(($i+180)*3.1418/180) * $Radius + $Y1 + $Radius; - - $Xi2 = cos(($i-90)*3.1418/180) * $Radius + $X2 - $Radius; - $Yi2 = sin(($i-90)*3.1418/180) * $Radius + $Y1 + $Radius; - - $Xi3 = cos(($i)*3.1418/180) * $Radius + $X2 - $Radius; - $Yi3 = sin(($i)*3.1418/180) * $Radius + $Y2 - $Radius; - - $Xi4 = cos(($i+90)*3.1418/180) * $Radius + $X1 + $Radius; - $Yi4 = sin(($i+90)*3.1418/180) * $Radius + $Y2 - $Radius; - - imageline($this->Picture,$Xi1,$Yi1,$X1+$Radius,$Yi1,$C_Rectangle); - imageline($this->Picture,$X2-$Radius,$Yi2,$Xi2,$Yi2,$C_Rectangle); - imageline($this->Picture,$X2-$Radius,$Yi3,$Xi3,$Yi3,$C_Rectangle); - imageline($this->Picture,$Xi4,$Yi4,$X1+$Radius,$Yi4,$C_Rectangle); - - $this->drawAntialiasPixel($Xi1,$Yi1,$R,$G,$B); - $this->drawAntialiasPixel($Xi2,$Yi2,$R,$G,$B); - $this->drawAntialiasPixel($Xi3,$Yi3,$R,$G,$B); - $this->drawAntialiasPixel($Xi4,$Yi4,$R,$G,$B); - } - - imagefilledrectangle($this->Picture,$X1,$Y1+$Radius,$X2,$Y2-$Radius,$C_Rectangle); - imagefilledrectangle($this->Picture,$X1+$Radius,$Y1,$X2-$Radius,$Y2,$C_Rectangle); - - $X1=$X1-.2;$Y1=$Y1-.2; - $X2=$X2+.2;$Y2=$Y2+.2; - $this->drawLine($X1+$Radius,$Y1,$X2-$Radius,$Y1,$R,$G,$B); - $this->drawLine($X2,$Y1+$Radius,$X2,$Y2-$Radius,$R,$G,$B); - $this->drawLine($X2-$Radius,$Y2,$X1+$Radius,$Y2,$R,$G,$B); - $this->drawLine($X1,$Y2-$Radius,$X1,$Y1+$Radius,$R,$G,$B); - } - - /* This function create a circle with antialias */ - function drawCircle($Xc,$Yc,$Height,$R,$G,$B,$Width=0) - { - if ( $Width == 0 ) { $Width = $Height; } - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_Circle = $this->AllocateColor($this->Picture,$R,$G,$B); - $Step = 360 / (2 * 3.1418 * max($Width,$Height)); - - for($i=0;$i<=360;$i=$i+$Step) - { - $X = cos($i*3.1418/180) * $Height + $Xc; - $Y = sin($i*3.1418/180) * $Width + $Yc; - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - } - } - - /* This function create a filled circle/ellipse with antialias */ - function drawFilledCircle($Xc,$Yc,$Height,$R,$G,$B,$Width=0) - { - if ( $Width == 0 ) { $Width = $Height; } - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $C_Circle = $this->AllocateColor($this->Picture,$R,$G,$B); - $Step = 360 / (2 * 3.1418 * max($Width,$Height)); - - for($i=90;$i<=270;$i=$i+$Step) - { - $X1 = cos($i*3.1418/180) * $Height + $Xc; - $Y1 = sin($i*3.1418/180) * $Width + $Yc; - $X2 = cos((180-$i)*3.1418/180) * $Height + $Xc; - $Y2 = sin((180-$i)*3.1418/180) * $Width + $Yc; - - $this->drawAntialiasPixel($X1-1,$Y1-1,$R,$G,$B); - $this->drawAntialiasPixel($X2-1,$Y2-1,$R,$G,$B); - - if ( ($Y1-1) > $Yc - max($Width,$Height) ) - imageline($this->Picture,$X1,$Y1-1,$X2-1,$Y2-1,$C_Circle); - } - } - - /* This function will draw a filled ellipse */ - function drawEllipse($Xc,$Yc,$Height,$Width,$R,$G,$B) - { $this->drawCircle($Xc,$Yc,$Height,$R,$G,$B,$Width); } - - /* This function will draw an ellipse */ - function drawFilledEllipse($Xc,$Yc,$Height,$Width,$R,$G,$B) - { $this->drawFilledCircle($Xc,$Yc,$Height,$R,$G,$B,$Width); } - - /* This function create a line with antialias */ - function drawLine($X1,$Y1,$X2,$Y2,$R,$G,$B,$GraphFunction=FALSE) - { - if ( $this->LineDotSize > 1 ) { $this->drawDottedLine($X1,$Y1,$X2,$Y2,$this->LineDotSize,$R,$G,$B,$GraphFunction); return(0); } - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $Distance = sqrt(($X2-$X1)*($X2-$X1)+($Y2-$Y1)*($Y2-$Y1)); - if ( $Distance == 0 ) - return(-1); - $XStep = ($X2-$X1) / $Distance; - $YStep = ($Y2-$Y1) / $Distance; - - for($i=0;$i<=$Distance;$i++) - { - $X = $i * $XStep + $X1; - $Y = $i * $YStep + $Y1; - - if ( ($X >= $this->GArea_X1 && $X <= $this->GArea_X2 && $Y >= $this->GArea_Y1 && $Y <= $this->GArea_Y2) || !$GraphFunction ) - { - if ( $this->LineWidth == 1 ) - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - else - { - $StartOffset = -($this->LineWidth/2); $EndOffset = ($this->LineWidth/2); - for($j=$StartOffset;$j<=$EndOffset;$j++) - $this->drawAntialiasPixel($X+$j,$Y+$j,$R,$G,$B); - } - } - } - } - - /* This function create a line with antialias */ - function drawDottedLine($X1,$Y1,$X2,$Y2,$DotSize,$R,$G,$B,$GraphFunction=FALSE) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $Distance = sqrt(($X2-$X1)*($X2-$X1)+($Y2-$Y1)*($Y2-$Y1)); - - $XStep = ($X2-$X1) / $Distance; - $YStep = ($Y2-$Y1) / $Distance; - - $DotIndex = 0; - for($i=0;$i<=$Distance;$i++) - { - $X = $i * $XStep + $X1; - $Y = $i * $YStep + $Y1; - - if ( $DotIndex <= $DotSize) - { - if ( ($X >= $this->GArea_X1 && $X <= $this->GArea_X2 && $Y >= $this->GArea_Y1 && $Y <= $this->GArea_Y2) || !$GraphFunction ) - { - if ( $this->LineWidth == 1 ) - $this->drawAntialiasPixel($X,$Y,$R,$G,$B); - else - { - $StartOffset = -($this->LineWidth/2); $EndOffset = ($this->LineWidth/2); - for($j=$StartOffset;$j<=$EndOffset;$j++) - $this->drawAntialiasPixel($X+$j,$Y+$j,$R,$G,$B); - } - } - } - - $DotIndex++; - if ( $DotIndex == $DotSize * 2 ) - $DotIndex = 0; - } - } - - /* Load a PNG file and draw it over the chart */ - function drawFromPNG($FileName,$X,$Y,$Alpha=100) - { $this->drawFromPicture(1,$FileName,$X,$Y,$Alpha); } - - /* Load a GIF file and draw it over the chart */ - function drawFromGIF($FileName,$X,$Y,$Alpha=100) - { $this->drawFromPicture(2,$FileName,$X,$Y,$Alpha); } - - /* Load a JPEG file and draw it over the chart */ - function drawFromJPG($FileName,$X,$Y,$Alpha=100) - { $this->drawFromPicture(3,$FileName,$X,$Y,$Alpha); } - - /* Generic loader function for external pictures */ - function drawFromPicture($PicType,$FileName,$X,$Y,$Alpha=100) - { - if ( file_exists($FileName)) - { - $Infos = getimagesize($FileName); - $Width = $Infos[0]; - $Height = $Infos[1]; - if ( $PicType == 1 ) { $Raster = imagecreatefrompng($FileName); } - if ( $PicType == 2 ) { $Raster = imagecreatefromgif($FileName); } - if ( $PicType == 3 ) { $Raster = imagecreatefromjpeg($FileName); } - - imagecopymerge($this->Picture,$Raster,$X,$Y,0,0,$Width,$Height,$Alpha); - imagedestroy($Raster); - } - } - - /* Draw an alpha pixel */ - function drawAlphaPixel($X,$Y,$Alpha,$R,$G,$B) - { - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - if ( $X < 0 || $Y < 0 || $X >= $this->XSize || $Y >= $this->YSize ) - return(-1); - - $RGB2 = imagecolorat($this->Picture, $X, $Y); - $R2 = ($RGB2 >> 16) & 0xFF; - $G2 = ($RGB2 >> 8) & 0xFF; - $B2 = $RGB2 & 0xFF; - - $iAlpha = (100 - $Alpha)/100; - $Alpha = $Alpha / 100; - - $Ra = floor($R*$Alpha+$R2*$iAlpha); - $Ga = floor($G*$Alpha+$G2*$iAlpha); - $Ba = floor($B*$Alpha+$B2*$iAlpha); - - $C_Aliased = $this->AllocateColor($this->Picture,$Ra,$Ga,$Ba); - imagesetpixel($this->Picture,$X,$Y,$C_Aliased); - } - - /* Color helper */ - function AllocateColor($Picture,$R,$G,$B,$Factor=0) - { - $R = $R + $Factor; - $G = $G + $Factor; - $B = $B + $Factor; - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - return(imagecolorallocate($Picture,$R,$G,$B)); - } - - /* Add a border to the picture */ - function addBorder($Size=3,$R=0,$G=0,$B=0) - { - $Width = $this->XSize+2*$Size; - $Height = $this->YSize+2*$Size; - - $Resampled = imagecreatetruecolor($Width,$Height); - $C_Background = $this->AllocateColor($Resampled,$R,$G,$B); - imagefilledrectangle($Resampled,0,0,$Width,$Height,$C_Background); - - imagecopy($Resampled,$this->Picture,$Size,$Size,0,0,$this->XSize,$this->YSize); - imagedestroy($this->Picture); - - $this->XSize = $Width; - $this->YSize = $Height; - - $this->Picture = imagecreatetruecolor($this->XSize,$this->YSize); - $C_White = $this->AllocateColor($this->Picture,255,255,255); - imagefilledrectangle($this->Picture,0,0,$this->XSize,$this->YSize,$C_White); - imagecolortransparent($this->Picture,$C_White); - imagecopy($this->Picture,$Resampled,0,0,0,0,$this->XSize,$this->YSize); - } - - /* Render the current picture to a file */ - function Render($FileName) - { - if ( $this->ErrorReporting ) - $this->printErrors($this->ErrorInterface); - - /* Save image map if requested */ - if ( $this->BuildMap ) - $this->SaveImageMap(); - - imagepng($this->Picture,$FileName); - } - - /* Render the current picture to STDOUT */ - function Stroke() - { - if ( $this->ErrorReporting ) - $this->printErrors("GD"); - - /* Save image map if requested */ - if ( $this->BuildMap ) - $this->SaveImageMap(); - - header('Content-type: image/png'); - imagepng($this->Picture); - } - - /* Private functions for internal processing */ - function drawAntialiasPixel($X,$Y,$R,$G,$B,$Alpha=100,$NoFallBack=FALSE) - { - /* Process shadows */ - if ( $this->ShadowActive && !$NoFallBack ) - { - $this->drawAntialiasPixel($X+$this->ShadowXDistance,$Y+$this->ShadowYDistance,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor,$this->ShadowAlpha,TRUE); - if ( $this->ShadowBlur != 0 ) - { - $AlphaDecay = ($this->ShadowAlpha / $this->ShadowBlur); - - for($i=1; $i<=$this->ShadowBlur; $i++) - $this->drawAntialiasPixel($X+$this->ShadowXDistance-$i/2,$Y+$this->ShadowYDistance-$i/2,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor,$this->ShadowAlpha-$AlphaDecay*$i,TRUE); - for($i=1; $i<=$this->ShadowBlur; $i++) - $this->drawAntialiasPixel($X+$this->ShadowXDistance+$i/2,$Y+$this->ShadowYDistance+$i/2,$this->ShadowRColor,$this->ShadowGColor,$this->ShadowBColor,$this->ShadowAlpha-$AlphaDecay*$i,TRUE); - } - } - - if ( $R < 0 ) { $R = 0; } if ( $R > 255 ) { $R = 255; } - if ( $G < 0 ) { $G = 0; } if ( $G > 255 ) { $G = 255; } - if ( $B < 0 ) { $B = 0; } if ( $B > 255 ) { $B = 255; } - - $Plot = ""; - $Xi = floor($X); - $Yi = floor($Y); - - if ( $Xi == $X && $Yi == $Y) - { - if ( $Alpha == 100 ) - { - $C_Aliased = $this->AllocateColor($this->Picture,$R,$G,$B); - imagesetpixel($this->Picture,$X,$Y,$C_Aliased); - } - else - $this->drawAlphaPixel($X,$Y,$Alpha,$R,$G,$B); - } - else - { - $Alpha1 = (((1 - ($X - floor($X))) * (1 - ($Y - floor($Y))) * 100) / 100) * $Alpha; - if ( $Alpha1 > $this->AntialiasQuality ) { $this->drawAlphaPixel($Xi,$Yi,$Alpha1,$R,$G,$B); } - - $Alpha2 = ((($X - floor($X)) * (1 - ($Y - floor($Y))) * 100) / 100) * $Alpha; - if ( $Alpha2 > $this->AntialiasQuality ) { $this->drawAlphaPixel($Xi+1,$Yi,$Alpha2,$R,$G,$B); } - - $Alpha3 = (((1 - ($X - floor($X))) * ($Y - floor($Y)) * 100) / 100) * $Alpha; - if ( $Alpha3 > $this->AntialiasQuality ) { $this->drawAlphaPixel($Xi,$Yi+1,$Alpha3,$R,$G,$B); } - - $Alpha4 = ((($X - floor($X)) * ($Y - floor($Y)) * 100) / 100) * $Alpha; - if ( $Alpha4 > $this->AntialiasQuality ) { $this->drawAlphaPixel($Xi+1,$Yi+1,$Alpha4,$R,$G,$B); } - } - } - - /* Validate data contained in the description array */ - function validateDataDescription($FunctionName,&$DataDescription,$DescriptionRequired=TRUE) - { - if (!isset($DataDescription["Position"])) - { - $this->Errors[] = "[Warning] ".$FunctionName." - Y Labels are not set."; - $DataDescription["Position"] = "Name"; - } - - if ( $DescriptionRequired ) - { - if (!isset($DataDescription["Description"])) - { - $this->Errors[] = "[Warning] ".$FunctionName." - Series descriptions are not set."; - foreach($DataDescription["Values"] as $key => $Value) - { - $DataDescription["Description"][$Value] = $Value; - } - } - - if (count($DataDescription["Description"]) < count($DataDescription["Values"])) - { - $this->Errors[] = "[Warning] ".$FunctionName." - Some series descriptions are not set."; - foreach($DataDescription["Values"] as $key => $Value) - { - if ( !isset($DataDescription["Description"][$Value])) - $DataDescription["Description"][$Value] = $Value; - } - } - } - } - - /* Validate data contained in the data array */ - function validateData($FunctionName,&$Data) - { - $DataSummary = array(); - - foreach($Data as $key => $Values) - { - foreach($Values as $key2 => $Value) - { - if (!isset($DataSummary[$key2])) - $DataSummary[$key2] = 1; - else - $DataSummary[$key2]++; - } - } - - if ( max($DataSummary) == 0 ) - $this->Errors[] = "[Warning] ".$FunctionName." - No data set."; - - foreach($DataSummary as $key => $Value) - { - if ($Value < max($DataSummary)) - { - $this->Errors[] = "[Warning] ".$FunctionName." - Missing data in serie ".$key."."; - } - } - } - - /* Print all error messages on the CLI or graphically */ - function printErrors($Mode="CLI") - { - if (count($this->Errors) == 0) - return(0); - - if ( $Mode == "CLI" ) - { - foreach($this->Errors as $key => $Value) - echo $Value."\r\n"; - } - elseif ( $Mode == "GD" ) - { - $this->setLineStyle($Width=1); - $MaxWidth = 0; - foreach($this->Errors as $key => $Value) - { - $Position = imageftbbox($this->ErrorFontSize,0,$this->ErrorFontName,$Value); - $TextWidth = $Position[2]-$Position[0]; - if ( $TextWidth > $MaxWidth ) { $MaxWidth = $TextWidth; } - } - $this->drawFilledRoundedRectangle($this->XSize-($MaxWidth+20),$this->YSize-(20+(($this->ErrorFontSize+4)*count($this->Errors))),$this->XSize-10,$this->YSize-10,6,233,185,185); - $this->drawRoundedRectangle($this->XSize-($MaxWidth+20),$this->YSize-(20+(($this->ErrorFontSize+4)*count($this->Errors))),$this->XSize-10,$this->YSize-10,6,193,145,145); - - $C_TextColor = $this->AllocateColor($this->Picture,133,85,85); - $YPos = $this->YSize - (18 + (count($this->Errors)-1) * ($this->ErrorFontSize + 4)); - foreach($this->Errors as $key => $Value) - { - imagettftext($this->Picture,$this->ErrorFontSize,0,$this->XSize-($MaxWidth+15),$YPos,$C_TextColor,$this->ErrorFontName,$Value); - $YPos = $YPos + ($this->ErrorFontSize + 4); - } - } - } - - /* Activate the image map creation process */ - function setImageMap($Mode=TRUE,$GraphID="MyGraph") - { - $this->BuildMap = $Mode; - $this->MapID = $GraphID; - } - - /* Add a box into the image map */ - function addToImageMap($X1,$Y1,$X2,$Y2,$SerieName,$Value,$CallerFunction) - { - $poly = array(array($X1,$Y1),array($X2,$Y1),array($X2,$Y2),array($X1,$Y2)); - $this->addPolyToImageMap($poly,$SerieName,$Value,$CallerFunction); - } - - function addPolyToImageMap($poly,$SerieName,$Value,$CallerFunction) - { - if ( $this->MapFunction == NULL || $this->MapFunction == $CallerFunction) - { - $this->ImageMap[] = array( - 'n' => (string)$SerieName, - 'v' => (string)$Value, - 'p' => $poly, - ); - $this->MapFunction = $CallerFunction; - } - } - - /* Draw image map to the current chart image */ - function debugImageMap() - { - foreach ($this->ImageMap as $polygon) - { - $points = array(); - foreach ($polygon['p'] as $point) - { - $points[] = $point[0]; - $points[] = $point[1]; - } - - $color = $this->AllocateColor($this->Picture,rand(0,255),rand(0,255),rand(0,255)); - imagefilledpolygon($this->Picture,$points,(count($points)+1)/2,$color); - } - - } - - /* Get the current image map */ - function getImageMap() - { - return $this->ImageMap; - } - - /* Load and cleanup the image map from disk */ - function getSavedImageMap($MapName,$Flush=TRUE) - { - /* Strip HTML query strings */ - $Values = $this->tmpFolder.$MapName; - $Value = split("\?",$Values); - $FileName = $Value[0]; - - if ( file_exists($FileName) ) - { - $Handle = fopen($FileName, "r"); - $MapContent = fread($Handle, filesize($FileName)); - fclose($Handle); - echo $MapContent; - - if ( $Flush ) - unlink($FileName); - - exit(); - } - else - { - header("HTTP/1.0 404 Not Found"); - exit(); - } - } - - /* Save the image map to the disk */ - function SaveImageMap() - { - if ( !$this->BuildMap ) { return(-1); } - - if ( $this->ImageMap == NULL ) - { - $this->Errors[] = "[Warning] SaveImageMap - Image map is empty."; - return(-1); - } - - $Handle = fopen($this->tmpFolder.$this->MapID, 'w'); - if ( !$Handle ) - { - $this->Errors[] = "[Warning] SaveImageMap - Cannot save the image map."; - return(-1); - } - else - { - fwrite($Handle, serialize($this->getImageMap())); - } - fclose ($Handle); - } - - /* Convert seconds to a time format string */ - function ToTime($Value) - { - $Hour = floor($Value/3600); - $Minute = floor(($Value - $Hour*3600)/60); - $Second = floor($Value - $Hour*3600 - $Minute*60); - - if (strlen($Hour) == 1 ) { $Hour = "0".$Hour; } - if (strlen($Minute) == 1 ) { $Minute = "0".$Minute; } - if (strlen($Second) == 1 ) { $Second = "0".$Second; } - - return($Hour.":".$Minute.":".$Second); - } - - /* Convert to metric system */ - function ToMetric($Value) - { - $Go = floor($Value/1000000000); - $Mo = floor(($Value - $Go*1000000000)/1000000); - $Ko = floor(($Value - $Go*1000000000 - $Mo*1000000)/1000); - $o = floor($Value - $Go*1000000000 - $Mo*1000000 - $Ko*1000); - - if ($Go != 0) { return($Go.".".$Mo."g"); } - if ($Mo != 0) { return($Mo.".".$ko."m"); } - if ($Ko != 0) { return($Ko.".".$o)."k"; } - return($o); - } - - /* Convert to curency */ - function ToCurrency($Value) - { - $Go = floor($Value/1000000000); - $Mo = floor(($Value - $Go*1000000000)/1000000); - $Ko = floor(($Value - $Go*1000000000 - $Mo*1000000)/1000); - $o = floor($Value - $Go*1000000000 - $Mo*1000000 - $Ko*1000); - - if ( strlen($o) == 1 ) { $o = "00".$o; } - if ( strlen($o) == 2 ) { $o = "0".$o; } - - $ResultString = $o; - if ( $Ko != 0 ) { $ResultString = $Ko.".".$ResultString; } - if ( $Mo != 0 ) { $ResultString = $Mo.".".$ResultString; } - if ( $Go != 0 ) { $ResultString = $Go.".".$ResultString; } - - $ResultString = $this->Currency.$ResultString; - return($ResultString); - } - - /* Set date format for axis labels */ - function setDateFormat($Format) - { - $this->DateFormat = $Format; - } - - /* Convert TS to a date format string */ - function ToDate($Value) - { - return(date($this->DateFormat,$Value)); - } - - /* Check if a number is a full integer (for scaling) */ - function isRealInt($Value) - { - if ($Value == floor($Value)) - return(TRUE); - return(FALSE); - } - } - - function RaiseFatal($Message) - { - echo "[FATAL] ".$Message."\r\n"; - exit(); - } -?> \ No newline at end of file diff --git a/libraries/chart/pChart/pData.class b/libraries/chart/pChart/pData.class deleted file mode 100644 index 1c4a301..0000000 --- a/libraries/chart/pChart/pData.class +++ /dev/null @@ -1,260 +0,0 @@ -<?php - /* - pData - Simplifying data population for pChart - Copyright (C) 2008 Jean-Damien POGOLOTTI - Version 1.13 last updated on 08/17/08 - - http://pchart.sourceforge.net - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 1,2,3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see <http://www.gnu.org/licenses/>. - - Class initialisation : - pData() - Data populating methods : - ImportFromCSV($FileName,$Delimiter=",",$DataColumns=-1,$HasHeader=FALSE,$DataName=-1) - AddPoint($Value,$Serie="Serie1",$Description="") - Series manipulation methods : - AddSerie($SerieName="Serie1") - AddAllSeries() - RemoveSerie($SerieName="Serie1") - SetAbsciseLabelSerie($SerieName = "Name") - SetSerieName($Name,$SerieName="Serie1") - + SetSerieSymbol($Name,$Symbol) - SetXAxisName($Name="X Axis") - SetYAxisName($Name="Y Axis") - SetXAxisFormat($Format="number") - SetYAxisFormat($Format="number") - SetXAxisUnit($Unit="") - SetYAxisUnit($Unit="") - removeSerieName($SerieName) - removeAllSeries() - Data retrieval methods : - GetData() - GetDataDescription() - */ - - /* pData class definition */ - class pData - { - var $Data; - var $DataDescription; - - function pData() - { - $this->Data = array(); - $this->DataDescription = ""; - $this->DataDescription["Position"] = "Name"; - $this->DataDescription["Format"]["X"] = "number"; - $this->DataDescription["Format"]["Y"] = "number"; - $this->DataDescription["Unit"]["X"] = NULL; - $this->DataDescription["Unit"]["Y"] = NULL; - } - - function ImportFromCSV($FileName,$Delimiter=",",$DataColumns=-1,$HasHeader=FALSE,$DataName=-1) - { - $handle = @fopen($FileName,"r"); - if ($handle) - { - $HeaderParsed = FALSE; - while (!feof($handle)) - { - $buffer = fgets($handle, 4096); - $buffer = str_replace(chr(10),"",$buffer); - $buffer = str_replace(chr(13),"",$buffer); - $Values = split($Delimiter,$buffer); - - if ( $buffer != "" ) - { - if ( $HasHeader == TRUE && $HeaderParsed == FALSE ) - { - if ( $DataColumns == -1 ) - { - $ID = 1; - foreach($Values as $key => $Value) - { $this->SetSerieName($Value,"Serie".$ID); $ID++; } - } - else - { - $SerieName = ""; - - foreach($DataColumns as $key => $Value) - $this->SetSerieName($Values[$Value],"Serie".$Value); - } - $HeaderParsed = TRUE; - } - else - { - if ( $DataColumns == -1 ) - { - $ID = 1; - foreach($Values as $key => $Value) - { $this->AddPoint(intval($Value),"Serie".$ID); $ID++; } - } - else - { - $SerieName = ""; - if ( $DataName != -1 ) - $SerieName = $Values[$DataName]; - - foreach($DataColumns as $key => $Value) - $this->AddPoint($Values[$Value],"Serie".$Value,$SerieName); - } - } - } - } - fclose($handle); - } - } - - function AddPoint($Value,$Serie="Serie1",$Description="") - { - if (is_array($Value) && count($Value) == 1) - $Value = array_pop($Value); - - $ID = 0; - for($i=0;$i<=count($this->Data);$i++) - { if(isset($this->Data[$i][$Serie])) { $ID = $i+1; } } - - if ( count($Value) == 1 ) - { - $this->Data[$ID][$Serie] = $Value; - if ( $Description != "" ) - $this->Data[$ID]["Name"] = $Description; - elseif (!isset($this->Data[$ID]["Name"])) - $this->Data[$ID]["Name"] = $ID; - } - else - { - foreach($Value as $key => $Val) - { - $this->Data[$ID][$Serie] = $Val; - if (!isset($this->Data[$ID]["Name"])) - $this->Data[$ID]["Name"] = $ID; - $ID++; - } - } - } - - function AddSerie($SerieName="Serie1") - { - if ( !isset($this->DataDescription["Values"]) ) - { - $this->DataDescription["Values"][] = $SerieName; - } - else - { - $Found = FALSE; - foreach($this->DataDescription["Values"] as $key => $Value ) - if ( $Value == $SerieName ) { $Found = TRUE; } - - if ( !$Found ) - $this->DataDescription["Values"][] = $SerieName; - } - } - - function AddAllSeries() - { - unset($this->DataDescription["Values"]); - - if ( isset($this->Data[0]) ) - { - foreach($this->Data[0] as $Key => $Value) - { - if ( $Key != "Name" ) - $this->DataDescription["Values"][] = $Key; - } - } - } - - function RemoveSerie($SerieName="Serie1") - { - if ( !isset($this->DataDescription["Values"]) ) - return(0); - - $Found = FALSE; - foreach($this->DataDescription["Values"] as $key => $Value ) - { - if ( $Value == $SerieName ) - unset($this->DataDescription["Values"][$key]); - } - } - - function SetAbsciseLabelSerie($SerieName = "Name") - { - $this->DataDescription["Position"] = $SerieName; - } - - function SetSerieName($Name,$SerieName="Serie1") - { - $this->DataDescription["Description"][$SerieName] = $Name; - } - - function SetXAxisName($Name="X Axis") - { - $this->DataDescription["Axis"]["X"] = $Name; - } - - function SetYAxisName($Name="Y Axis") - { - $this->DataDescription["Axis"]["Y"] = $Name; - } - - function SetXAxisFormat($Format="number") - { - $this->DataDescription["Format"]["X"] = $Format; - } - - function SetYAxisFormat($Format="number") - { - $this->DataDescription["Format"]["Y"] = $Format; - } - - function SetXAxisUnit($Unit="") - { - $this->DataDescription["Unit"]["X"] = $Unit; - } - - function SetYAxisUnit($Unit="") - { - $this->DataDescription["Unit"]["Y"] = $Unit; - } - - function SetSerieSymbol($Name,$Symbol) - { - $this->DataDescription["Symbol"][$Name] = $Symbol; - } - - function removeSerieName($SerieName) - { - if ( isset($this->DataDescription["Description"][$SerieName]) ) - unset($this->DataDescription["Description"][$SerieName]); - } - - function removeAllSeries() - { - foreach($this->DataDescription["Values"] as $Key => $Value) - unset($this->DataDescription["Values"][$Key]); - } - - function GetData() - { - return($this->Data); - } - - function GetDataDescription() - { - return($this->DataDescription); - } - } -?> \ No newline at end of file diff --git a/libraries/chart/pma_chart.php b/libraries/chart/pma_chart.php deleted file mode 100644 index 6708804..0000000 --- a/libraries/chart/pma_chart.php +++ /dev/null @@ -1,183 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * Holds the base class that all charts inherit from and some widely used - * constants. - * @package phpMyAdmin - */ - -/** - * - */ -define('RED', 0); -define('GREEN', 1); -define('BLUE', 2); - -/** - * The base class that all charts inherit from. - * @abstract - * @package phpMyAdmin - */ -abstract class PMA_chart -{ - /** - * @var array All the default settigs values are here. - */ - protected $settings = array( - - // Default title for every chart. - 'titleText' => 'Chart', - - // The style of the chart title. - 'titleColor' => '#FAFAFA', - - // Colors for the different slices in the pie chart. - 'colors' => array( - '#BCE02E', - '#E0642E', - '#E0D62E', - '#2E97E0', - '#B02EE0', - '#E02E75', - '#5CE02E', - '#E0B02E', - '#000000', - '#0022E0', - '#726CB1', - '#481A36', - '#BAC658', - '#127224', - '#825119', - '#238C74', - '#4C489B', - '#87C9BF', - ), - - // Chart background color. - 'bgColor' => '#84AD83', - - // The width of the chart. - 'width' => 520, - - // The height of the chart. - 'height' => 325, - - // Default X Axis label. If empty, label will be taken from the data. - 'xLabel' => '', - - // Default Y Axis label. If empty, label will be taken from the data. - 'yLabel' => '', - ); - - /** - * @var array Options that the user has specified - */ - private $userSpecifiedSettings = null; - - /** - * @var array Error codes will be stored here - */ - protected $errors = array(); - - /** - * Store user specified options - * @param array $options users specified options - */ - function __construct($options = null) - { - $this->userSpecifiedSettings = $options; - } - - /** - * All the variable initialization has to be done here. - */ - protected function init() - { - $this->handleOptions(); - } - - /** - * A function which handles passed parameters. Useful if desired - * chart needs to be a little bit different from the default one. - */ - private function handleOptions() - { - if (is_null($this->userSpecifiedSettings)) { - return; - } - - $this->settings = array_merge($this->settings, $this->userSpecifiedSettings); - } - - protected function getTitleText() - { - return $this->settings['titleText']; - } - - protected function getTitleColor($component) - { - return $this->hexStrToDecComp($this->settings['titleColor'], $component); - } - - protected function getColors() - { - return $this->settings['colors']; - } - - protected function getWidth() - { - return $this->settings['width']; - } - - protected function getHeight() - { - return $this->settings['height']; - } - - protected function getBgColor($component) - { - return $this->hexStrToDecComp($this->settings['bgColor'], $component); - } - - protected function setXLabel($label) - { - $this->settings['xLabel'] = $label; - } - - protected function getXLabel() - { - return $this->settings['xLabel']; - } - - protected function setYLabel($label) - { - $this->settings['yLabel'] = $label; - } - - protected function getYLabel() - { - return $this->settings['yLabel']; - } - - public function getSettings() - { - return $this->settings; - } - - public function getErrors() - { - return $this->errors; - } - - /** - * Get one the dec color component from the hex color string - * @param string $colorString color string, i.e. #5F22A99 - * @param int $component color component to get, i.e. 0 gets red. - */ - protected function hexStrToDecComp($colorString, $component) - { - return hexdec(substr($colorString, ($component * 2) + 1, 2)); - } -} - -?> diff --git a/libraries/chart/pma_pchart_chart.php b/libraries/chart/pma_pchart_chart.php deleted file mode 100644 index e68a9b3..0000000 --- a/libraries/chart/pma_pchart_chart.php +++ /dev/null @@ -1,402 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * Holds the base class that all charts using pChart inherit from and some - * widely used constants - * @package phpMyAdmin - */ - -/** - * - */ -define('TOP', 0); -define('RIGHT', 1); -define('BOTTOM', 2); -define('LEFT', 3); - -require_once 'pma_chart.php'; - -require_once 'pChart/pData.class'; -require_once 'pChart/pChart.class'; - -/** - * Base class for every chart implemented using pChart. - * @abstract - * @package phpMyAdmin - */ -abstract class PMA_pChart_chart extends PMA_chart -{ - /** - * @var String title text - */ - protected $titleText; - - /** - * @var array data for the chart - */ - protected $data; - - /** - * @var object pData object that holds the description of the data - */ - protected $dataSet; - - /** - * @var object pChart object that holds the chart - */ - protected $chart; - - /** - * @var array holds base64 encoded chart image parts - */ - protected $partsEncoded = array(); - - public function __construct($data, $options = null) - { - parent::__construct($options); - - $this->data = $data; - - $this->settings['fontPath'] = './libraries/chart/pChart/fonts/'; - - $this->settings['scale'] = SCALE_ADDALLSTART0; - - $this->settings['labelHeight'] = 20; - - $this->settings['fontSize'] = 8; - - $this->settings['continuous'] = 'off'; - - // as in CSS (top, right, bottom, left) - $this->setAreaMargins(array(20, 20, 40, 60)); - - // Get color settings from theme - $this->settings = array_merge($this->settings,$GLOBALS['cfg']['chartColor']); - } - - protected function init() - { - parent::init(); - - // create pChart object - $this->chart = new pChart($this->getWidth(), $this->getHeight()); - - // create pData object - $this->dataSet = new pData; - - $this->chart->reportWarnings('GD'); - $this->chart->ErrorFontName = $this->getFontPath().'DejaVuSans.ttf'; - - // initialize colors - foreach ($this->getColors() as $key => $color) { - $this->chart->setColorPalette( - $key, - hexdec(substr($color, 1, 2)), - hexdec(substr($color, 3, 2)), - hexdec(substr($color, 5, 2)) - ); - } - - $this->chart->setFontProperties($this->getFontPath().'DejaVuSans.ttf', $this->getFontSize()); - - $this->chart->setImageMap(true, 'mapid'); - } - - /** - * data is put to the $dataSet object according to what type chart is - * @abstract - */ - abstract protected function prepareDataSet(); - - /** - * all components of the chart are drawn - */ - protected function prepareChart() - { - $this->drawBackground(); - $this->drawChart(); - } - - /** - * draws the background - */ - protected function drawBackground() - { - $this->drawCommon(); - $this->drawTitle(); - $this->setGraphAreaDimensions(); - $this->drawGraphArea(); - } - - /** - * draws the part of the background which is common to most of the charts - */ - protected function drawCommon() - { - $this->chart->drawGraphAreaGradient( - $this->getBgColor(RED), - $this->getBgColor(GREEN), - $this->getBgColor(BLUE), - // With a gradientIntensity of 0 the background does't draw, oddly - ($this->settings['gradientIntensity']==0)?1:$this->settings['gradientIntensity'],TARGET_BACKGROUND); - - if(is_string($this->settings['border'])) - $this->chart->addBorder(1,$this->getBorderColor(RED),$this->getBorderColor(GREEN),$this->getBorderColor(BLUE)); - } - - /** - * draws the chart title - */ - protected function drawTitle() - { - // Draw the title - $this->chart->drawTextBox( - 0, - 0, - $this->getWidth(), - $this->getLabelHeight(), - $this->getTitleText(), - 0, - $this->getTitleColor(RED), - $this->getTitleColor(GREEN), - $this->getTitleColor(BLUE), - ALIGN_CENTER, - false, - $this->getTitleBgColor(RED), - $this->getTitleBgColor(GREEN), - $this->getTitleBgColor(BLUE) - ); - } - - /** - * calculates and sets the dimensions that will be used for the actual graph - */ - protected function setGraphAreaDimensions() - { - $this->chart->setGraphArea( - $this->getAreaMargin(LEFT), - $this->getLabelHeight() + $this->getAreaMargin(TOP), - $this->getWidth() - $this->getAreaMargin(RIGHT), - $this->getHeight() - $this->getAreaMargin(BOTTOM) - ); - } - - /** - * draws graph area (the area where all bars, lines, points will be seen) - */ - protected function drawGraphArea() - { - $this->chart->drawGraphArea( - $this->getGraphAreaColor(RED), - $this->getGraphAreaColor(GREEN), - $this->getGraphAreaColor(BLUE), - false - ); - $this->chart->drawScale( - $this->dataSet->GetData(), - $this->dataSet->GetDataDescription(), - $this->getScale(), - $this->getScaleColor(RED), - $this->getScaleColor(GREEN), - $this->getScaleColor(BLUE), - true,0,2,true - ); - - if($this->settings['gradientIntensity']>0) - $this->chart->drawGraphAreaGradient( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE), - $this->settings['gradientIntensity'] - ); - else - $this->chart->drawGraphArea( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE) - ); - - $this->chart->drawGrid( - 4, - true, - $this->getGridColor(RED), - $this->getGridColor(GREEN), - $this->getGridColor(BLUE), - 20 - ); - } - - /** - * draws the chart - * @abstract - */ - protected abstract function drawChart(); - - /** - * Renders the chart, base 64 encodes the output and puts it into - * array partsEncoded. - * - * Parameter can be used to slice the chart vertically into parts. This - * solves an issue where some browsers (IE8) accept base64 images only up - * to some length. - * - * @param integer $parts number of parts to render. - * Default value 1 means that all the - * chart will be in one piece. - */ - protected function render($parts = 1) - { - $fullWidth = 0; - - for ($i = 0; $i < $parts; $i++) { - - // slicing is vertical so part height is the full height - $partHeight = $this->chart->YSize; - - // there will be some rounding erros, will compensate later - $partWidth = round($this->chart->XSize / $parts); - $fullWidth += $partWidth; - $partX = $partWidth * $i; - - if ($i == $parts - 1) { - // if this is the last part, compensate for the rounding errors - $partWidth += $this->chart->XSize - $fullWidth; - } - - // get a part from the full chart image - $part = imagecreatetruecolor($partWidth, $partHeight); - imagecopy($part, $this->chart->Picture, 0, 0, $partX, 0, $partWidth, $partHeight); - - // render part and save it to variable - ob_start(); - imagepng($part, NULL, 9, PNG_ALL_FILTERS); - $output = ob_get_contents(); - ob_end_clean(); - - // base64 encode the current part - $partEncoded = base64_encode($output); - $this->partsEncoded[$i] = $partEncoded; - } - } - - /** - * get the HTML and JS code for the configured chart - * @return string HTML and JS code for the chart - */ - public function toString() - { - if (!function_exists('gd_info')) { - array_push($this->errors, ERR_NO_GD); - return ''; - } - - $this->init(); - $this->prepareDataSet(); - $this->prepareChart(); - - //$this->chart->debugImageMap(); - //$this->chart->printErrors('GD'); - - // check if a user wanted a chart in one part - if ($this->isContinuous()) { - $this->render(1); - } - else { - $this->render(20); - } - - $returnData = '<div id="chart">'; - foreach ($this->partsEncoded as $part) { - $returnData .= '<img src="data:image/png;base64,'.$part.'" />'; - } - $returnData .= '</div>'; - - // add tooltips only if json is available - if (function_exists('json_encode')) { - $returnData .= ' - <script type="text/javascript"> - //<![CDATA[ - imageMap.loadImageMap(\''.json_encode($this->getImageMap()).'\'); - //]]> - </script> - '; - } - else { - array_push($this->errors, ERR_NO_JSON); - } - - return $returnData; - } - - protected function getLabelHeight() - { - return $this->settings['labelHeight']; - } - - protected function setAreaMargins($areaMargins) - { - $this->settings['areaMargins'] = $areaMargins; - } - - protected function getAreaMargin($side) - { - return $this->settings['areaMargins'][$side]; - } - - protected function getFontPath() - { - return $this->settings['fontPath']; - } - - protected function getScale() - { - return $this->settings['scale']; - } - - protected function getFontSize() - { - return $this->settings['fontSize']; - } - - protected function isContinuous() - { - return $this->settings['continuous'] == 'on'; - } - - protected function getImageMap() - { - return $this->chart->getImageMap(); - } - - protected function getGraphAreaColor($component) - { - return $this->hexStrToDecComp($this->settings['graphAreaColor'], $component); - } - - protected function getGraphAreaGradientColor($component) - { - return $this->hexStrToDecComp($this->settings['graphAreaGradientColor'], $component); - } - - protected function getGridColor($component) - { - return $this->hexStrToDecComp($this->settings['gridColor'], $component); - } - - protected function getScaleColor($component) - { - return $this->hexStrToDecComp($this->settings['scaleColor'], $component); - } - - protected function getTitleBgColor($component) - { - return $this->hexStrToDecComp($this->settings['titleBgColor'], $component); - } - - protected function getBorderColor($component) - { - return $this->hexStrToDecComp($this->settings['border'], $component); - } -} - -?> diff --git a/libraries/chart/pma_pchart_multi.php b/libraries/chart/pma_pchart_multi.php deleted file mode 100644 index 171cc50..0000000 --- a/libraries/chart/pma_pchart_multi.php +++ /dev/null @@ -1,117 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_chart.php'; - -/** - * Base class for every chart that uses multiple series. - * All of these charts will require legend box. - * @abstract - * @package phpMyAdmin - */ -abstract class PMA_pChart_multi extends PMA_pChart_chart -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - - // as in CSS (top, right, bottom, left) - $this->setLegendMargins(array(20, 10, 0, 0)); - } - - /** - * data set preparation for multi serie graphs - */ - protected function prepareDataSet() - { - $values = array_values($this->data); - $keys = array_keys($this->data); - - // Dataset definition - $this->dataSet->AddPoint($values[0], "Keys"); - - $i = 0; - foreach ($values[1] as $seriesName => $seriesData) { - $this->dataSet->AddPoint($seriesData, "Values".$i); - $this->dataSet->SetSerieName($seriesName, "Values".$i); - $i++; - } - $this->dataSet->AddAllSeries(); - - $this->dataSet->RemoveSerie("Keys"); - $this->dataSet->SetAbsciseLabelSerie("Keys"); - - $xLabel = $this->getXLabel(); - if (empty($xLabel)) { - $this->setXLabel($keys[0]); - } - $yLabel = $this->getYLabel(); - if (empty($yLabel)) { - $this->setYLabel($keys[1]); - } - - $this->dataSet->SetXAxisName($this->getXLabel()); - $this->dataSet->SetYAxisName($this->getYLabel()); - } - - /** - * set graph area dimensions with respect to legend box size - */ - protected function setGraphAreaDimensions() - { - $this->chart->setGraphArea( - $this->getAreaMargin(LEFT), - $this->getLabelHeight() + $this->getAreaMargin(TOP), - $this->getWidth() - $this->getAreaMargin(RIGHT) - $this->getLegendBoxWidth() - $this->getLegendMargin(LEFT) - $this->getLegendMargin(RIGHT), - $this->getHeight() - $this->getAreaMargin(BOTTOM) - ); - } - - /** - * multi serie charts need a legend. draw it - */ - protected function drawChart() - { - $this->drawLegend(); - } - - /** - * draws a legend - */ - protected function drawLegend() - { - // Draw the legend - $this->chart->drawLegend( - $this->getWidth() - $this->getLegendMargin(RIGHT) - $this->getLegendBoxWidth(), - $this->getLabelHeight() + $this->getLegendMargin(TOP), - $this->dataSet->GetDataDescription(), - 250,250,250,50,50,50 - ); - } - - protected function setLegendMargins($legendMargins) - { - if (!isset($this->settings['legendMargins'])) { - $this->settings['legendMargins'] = $legendMargins; - } - } - - protected function getLegendMargin($side) - { - return $this->settings['legendMargins'][$side]; - } - - protected function getLegendBoxWidth() - { - $legendSize = $this->chart->getLegendBoxSize($this->dataSet->GetDataDescription()); - return $legendSize[0]; - } -} - -?> diff --git a/libraries/chart/pma_pchart_multi_bar.php b/libraries/chart/pma_pchart_multi_bar.php deleted file mode 100644 index 619ef1a..0000000 --- a/libraries/chart/pma_pchart_multi_bar.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_multi.php'; - -/** - * implements multi bar chart - * @package phpMyAdmin - */ -class PMA_pChart_multi_bar extends PMA_pChart_multi -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - - $this->settings['scale'] = SCALE_NORMAL; - } - - /** - * draws multi bar graph - */ - protected function drawChart() - { - parent::drawChart(); - - // Draw the bar chart - $this->chart->drawBarGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 70); - } -} - -?> diff --git a/libraries/chart/pma_pchart_multi_line.php b/libraries/chart/pma_pchart_multi_line.php deleted file mode 100644 index 502f386..0000000 --- a/libraries/chart/pma_pchart_multi_line.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_multi.php'; - -/** - * implements multi line chart - * @package phpMyAdmin - */ -class PMA_pChart_multi_line extends PMA_pChart_multi -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - - $this->settings['scale'] = SCALE_NORMAL; - } - - /** - * draws multi line chart - */ - protected function drawChart() - { - parent::drawChart(); - - // Draw the bar chart - $this->chart->drawLineGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription()); - $this->chart->drawPlotGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 3, 1, -1, -1, -1, true); - } -} - -?> diff --git a/libraries/chart/pma_pchart_multi_radar.php b/libraries/chart/pma_pchart_multi_radar.php deleted file mode 100644 index 8c32cb3..0000000 --- a/libraries/chart/pma_pchart_multi_radar.php +++ /dev/null @@ -1,107 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_multi.php'; - -/** - * implements multi radar chart - * @package phpMyAdmin - */ -class PMA_pChart_multi_radar extends PMA_pChart_multi -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - - $this->normalizeValues(); - } - - /** - * Get the largest value from the data and normalize all the other values. - */ - private function normalizeValues() - { - $maxValue = 0; - $keys = array_keys($this->data); - $valueKey = $keys[1]; - - // get the max value - foreach ($this->data[$valueKey] as $values) { - if (max($values) > $maxValue) { - $maxValue = max($values); - } - } - - // normalize all the values according to the max value - foreach ($this->data[$valueKey] as &$values) { - foreach ($values as &$value) { - $value = $value / $maxValue * 10; - } - } - } - - /** - * graph area for the radar chart does not include grid lines - */ - protected function drawGraphArea() - { - $this->chart->drawGraphArea( - $this->getGraphAreaColor(RED), - $this->getGraphAreaColor(GREEN), - $this->getGraphAreaColor(BLUE), - false - ); - - if($this->settings['gradientIntensity']>0) - $this->chart->drawGraphAreaGradient( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE), - $this->settings['gradientIntensity'] - ); - else - $this->chart->drawGraphArea( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE) - ); - } - - /** - * draw multi radar chart - */ - protected function drawChart() - { - parent::drawChart(); - - // when drawing radar graph we can specify the border from the top of - // graph area. We want border to be dynamic, so that either the top - // or the side of the radar is some distance away from the top or the - // side of the graph area. - $areaWidth = $this->chart->GArea_X2 - $this->chart->GArea_X1; - $areaHeight = $this->chart->GArea_Y2 - $this->chart->GArea_Y1; - - if ($areaHeight > $areaWidth) { - $borderOffset = ($areaHeight - $areaWidth) / 2; - } - else { - $borderOffset = 0; - } - - // the least ammount that radar is away from the graph area side. - $borderOffset += 40; - - // Draw the radar chart - $this->chart->drawRadarAxis($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), true, $borderOffset, - 120, 120, 120, 230, 230, 230, -1, 2); - $this->chart->drawFilledRadar($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 50, $borderOffset); - } -} - -?> diff --git a/libraries/chart/pma_pchart_pie.php b/libraries/chart/pma_pchart_pie.php deleted file mode 100644 index 56148e0..0000000 --- a/libraries/chart/pma_pchart_pie.php +++ /dev/null @@ -1,109 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_multi.php'; - -/** - * implements pie chart - * @package phpMyAdmin - */ -class PMA_pChart_Pie extends PMA_pChart_multi -{ - public function __construct($data, $options = null) - { - // limit data size, no more than 18 pie slices - $data = array_slice($data, 0, 18, true); - parent::__construct($data, $options); - - $this->setAreaMargins(array(20, 10, 20, 20)); - } - - /** - * prepare data set for the pie chart - */ - protected function prepareDataSet() - { - // Dataset definition - $this->dataSet->AddPoint(array_values($this->data), "Values"); - $this->dataSet->AddPoint(array_keys($this->data), "Keys"); - $this->dataSet->AddAllSeries(); - $this->dataSet->SetAbsciseLabelSerie("Keys"); - } - - /** - * graph area for the pie chart does not include grid lines - */ - protected function drawGraphArea() - { - $this->chart->drawGraphArea( - $this->getGraphAreaColor(RED), - $this->getGraphAreaColor(GREEN), - $this->getGraphAreaColor(BLUE), - false - ); - - if($this->settings['gradientIntensity']>0) - $this->chart->drawGraphAreaGradient( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE), - $this->settings['gradientIntensity'] - ); - else - $this->chart->drawGraphArea( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE) - ); - - } - - /** - * draw the pie chart - */ - protected function drawChart() - { - parent::drawChart(); - - // draw pie chart in the middle of graph area - $middleX = ($this->chart->GArea_X1 + $this->chart->GArea_X2) / 2; - $middleY = ($this->chart->GArea_Y1 + $this->chart->GArea_Y2) / 2; - - $this->chart->drawPieGraph( - $this->dataSet->GetData(), - $this->dataSet->GetDataDescription(), - $middleX, - // pie graph is skewed. Upper part is shorter than the - // lower part. This is why we set an offset to the - // Y middle coordiantes. - $middleY - 15, - 120, PIE_PERCENTAGE, false, 60, 30, 10, 1); - } - - /** - * draw legend for the pie chart - */ - protected function drawLegend() - { - $this->chart->drawPieLegend( - $this->getWidth() - $this->getLegendMargin(RIGHT) - $this->getLegendBoxWidth(), - $this->getLabelHeight() + $this->getLegendMargin(TOP), - $this->dataSet->GetData(), - $this->dataSet->GetDataDescription(), - 250, 250, 250); - } - - protected function getLegendBoxWidth() - { - $legendSize = $this->chart->getPieLegendBoxSize($this->dataSet->GetData()); - return $legendSize[0]; - } -} - -?> diff --git a/libraries/chart/pma_pchart_single.php b/libraries/chart/pma_pchart_single.php deleted file mode 100644 index 6579a9b..0000000 --- a/libraries/chart/pma_pchart_single.php +++ /dev/null @@ -1,56 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_chart.php'; - -/** - * Base class for every chart that uses only one series. - * @abstract - * @package phpMyAdmin - */ -abstract class PMA_pChart_single extends PMA_pChart_chart -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - } - - /** - * data set preparation for single serie charts - */ - protected function prepareDataSet() - { - $values = array_values($this->data); - $keys = array_keys($this->data); - - // Dataset definition - $this->dataSet->AddPoint($values[0], "Values"); - $this->dataSet->AddPoint($values[1], "Keys"); - - //$this->dataSet->AddAllSeries(); - $this->dataSet->AddSerie("Values"); - - $this->dataSet->SetAbsciseLabelSerie("Keys"); - - $yLabel = $this->getYLabel(); - if (empty($yLabel)) { - $this->setYLabel($keys[0]); - } - $xLabel = $this->getXLabel(); - if (empty($xLabel)) { - $this->setXLabel($keys[1]); - } - - $this->dataSet->SetXAxisName($this->getXLabel()); - $this->dataSet->SetYAxisName($this->getYLabel()); - $this->dataSet->SetSerieName($this->getYLabel(), "Values"); - } -} - -?> diff --git a/libraries/chart/pma_pchart_single_bar.php b/libraries/chart/pma_pchart_single_bar.php deleted file mode 100644 index e651970..0000000 --- a/libraries/chart/pma_pchart_single_bar.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_single.php'; - -/** - * implements single bar chart - * @package phpMyAdmin - */ -class PMA_pChart_single_bar extends PMA_pChart_single -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - } - - /** - * draws single bar chart - */ - protected function drawChart() - { - // Draw the bar chart - // use stacked bar graph function, because it gives bars with alpha - $this->chart->drawStackedBarGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 70); - } -} - -?> diff --git a/libraries/chart/pma_pchart_single_line.php b/libraries/chart/pma_pchart_single_line.php deleted file mode 100644 index 3003581..0000000 --- a/libraries/chart/pma_pchart_single_line.php +++ /dev/null @@ -1,34 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_single.php'; - -/** - * implements single line chart - * @package phpMyAdmin - */ -class PMA_pChart_single_line extends PMA_pChart_single -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - } - - /** - * draws single line chart - */ - protected function drawChart() - { - // Draw the line chart - $this->chart->drawLineGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription()); - $this->chart->drawPlotGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 3, 1, -1, -1, -1, true); - } -} - -?> diff --git a/libraries/chart/pma_pchart_single_radar.php b/libraries/chart/pma_pchart_single_radar.php deleted file mode 100644 index 0c5d9ec..0000000 --- a/libraries/chart/pma_pchart_single_radar.php +++ /dev/null @@ -1,96 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_single.php'; - -/** - * implements single radar chart - * @package phpMyAdmin - */ -class PMA_pChart_single_radar extends PMA_pChart_single -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - - $this->normalizeValues(); - } - - /** - * Get the largest value from the data and normalize all the other values. - */ - private function normalizeValues() - { - $maxValue = 0; - $keys = array_keys($this->data); - $valueKey = $keys[0]; - $maxValue = max($this->data[$valueKey]); - - foreach ($this->data[$valueKey] as &$value) { - $value = $value / $maxValue * 10; - } - } - - /** - * graph area for the radar chart does not include grid lines - */ - protected function drawGraphArea() - { - $this->chart->drawGraphArea( - $this->getGraphAreaColor(RED), - $this->getGraphAreaColor(GREEN), - $this->getGraphAreaColor(BLUE), - false - ); - - if($this->settings['gradientIntensity']>0) - $this->chart->drawGraphAreaGradient( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE), - $this->settings['gradientIntensity'] - ); - else - $this->chart->drawGraphArea( - $this->getGraphAreaGradientColor(RED), - $this->getGraphAreaGradientColor(GREEN), - $this->getGraphAreaGradientColor(BLUE) - ); - - } - - /** - * draws the radar chart - */ - protected function drawChart() - { - // when drawing radar graph we can specify the border from the top of - // graph area. We want border to be dynamic, so that either the top - // or the side of the radar is some distance away from the top or the - // side of the graph area. - $areaWidth = $this->chart->GArea_X2 - $this->chart->GArea_X1; - $areaHeight = $this->chart->GArea_Y2 - $this->chart->GArea_Y1; - - if ($areaHeight > $areaWidth) { - $borderOffset = ($areaHeight - $areaWidth) / 2; - } - else { - $borderOffset = 0; - } - - // the least ammount that radar is away from the graph area side. - $borderOffset += 40; - - $this->chart->drawRadarAxis($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), - true, $borderOffset, 120, 120, 120, 230, 230, 230, -1, 2); - $this->chart->drawFilledRadar($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 50, $borderOffset); - } -} - -?> diff --git a/libraries/chart/pma_pchart_stacked_bar.php b/libraries/chart/pma_pchart_stacked_bar.php deleted file mode 100644 index 0ef72ec..0000000 --- a/libraries/chart/pma_pchart_stacked_bar.php +++ /dev/null @@ -1,35 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * @package phpMyAdmin - */ - -/** - * - */ -require_once 'pma_pchart_multi.php'; - -/** - * implements stacked bar chart - * @package phpMyAdmin - */ -class PMA_pChart_stacked_bar extends PMA_pChart_multi -{ - public function __construct($data, $options = null) - { - parent::__construct($data, $options); - } - - /** - * draws stacked bar chart - */ - protected function drawChart() - { - parent::drawChart(); - - // Draw the bar chart - $this->chart->drawStackedBarGraph($this->dataSet->GetData(), $this->dataSet->GetDataDescription(), 70); - } -} - -?> diff --git a/libraries/common.lib.php b/libraries/common.lib.php index 999174b..037656e 100644 --- a/libraries/common.lib.php +++ b/libraries/common.lib.php @@ -1303,43 +1303,6 @@ function PMA_profilingCheckbox($sql_query) }
/** - * Displays the results of SHOW PROFILE - * - * @param array the results - * @param boolean show chart - * @access public - * - */ -function PMA_profilingResults($profiling_results, $show_chart = false) -{ - echo '<fieldset><legend>' . __('Profiling') . '</legend>' . "\n"; - echo '<div style="float: left;">'; - echo '<table>' . "\n"; - echo ' <tr>' . "\n"; - echo ' <th>' . __('Status') . '</th>' . "\n"; - echo ' <th>' . __('Time') . '</th>' . "\n"; - echo ' </tr>' . "\n"; - - foreach($profiling_results as $one_result) { - echo ' <tr>' . "\n"; - echo '<td>' . $one_result['Status'] . '</td>' . "\n"; - echo '<td>' . $one_result['Duration'] . '</td>' . "\n"; - } - - echo '</table>' . "\n"; - echo '</div>'; - - if ($show_chart) { - require_once './libraries/chart.lib.php'; - echo '<div style="float: left;">'; - PMA_chart_profiling($profiling_results); - echo '</div>'; - } - - echo '</fieldset>' . "\n"; -} - -/** * Formats $value to byte view * * @param double $value the value to format @@ -1402,7 +1365,6 @@ function PMA_localizeNumber($value)
/** * Formats $value to the given length and appends SI prefixes - * $comma is not substracted from the length * with a $length of 0 no truncation occurs, number is only formated * to the current locale * @@ -1416,10 +1378,11 @@ function PMA_localizeNumber($value) * echo PMA_formatNumber(0, 6); // 0 * * </code> - * @param double $value the value to format - * @param integer $length the max length - * @param integer $comma the number of decimals to retain - * @param boolean $only_down do not reformat numbers below 1 + * @param double $value the value to format + * @param integer $digits_left number of digits left of the comma + * @param integer $digits_right number of digits right of the comma + * @param boolean $only_down do not reformat numbers below 1 + * @param boolean $noTrailingZero removes trailing zeros right of the comma (default: true) * * @return string the formatted value and its unit * @@ -1427,13 +1390,15 @@ function PMA_localizeNumber($value) * * @version 1.1.0 - 2005-10-27 */ -function PMA_formatNumber($value, $length = 3, $comma = 0, $only_down = false) +function PMA_formatNumber($value, $digits_left = 3, $digits_right = 0, $only_down = false, $noTrailingZero = true) { + if($value==0) return '0'; + $originalValue = $value; //number_format is not multibyte safe, str_replace is safe - if ($length === 0) { - $value = number_format($value, $comma); - if($originalValue!=0 && floatval($value) == 0) $value = ' <'.(1/PMA_pow(10,$comma)); + if ($digits_left === 0) { + $value = number_format($value, $digits_right); + if($originalValue!=0 && floatval($value) == 0) $value = ' <'.(1/PMA_pow(10,$digits_right));
return PMA_localizeNumber($value); } @@ -1459,11 +1424,6 @@ function PMA_formatNumber($value, $length = 3, $comma = 0, $only_down = false) 8 => 'Y' );
- // we need at least 3 digits to be displayed - if (3 > $length + $comma) { - $length = 3 - $comma; - } - // check for negative value to retain sign if ($value < 0) { $sign = '-'; @@ -1472,33 +1432,29 @@ function PMA_formatNumber($value, $length = 3, $comma = 0, $only_down = false) $sign = ''; }
- $dh = PMA_pow(10, $comma); - $li = PMA_pow(10, $length); - $unit = $units[0]; - - if ($value >= 1) { - for ($d = 8; $d >= 0; $d--) { - if (isset($units[$d]) && $value >= $li * PMA_pow(1000, $d-1)) { - $value = round($value / (PMA_pow(1000, $d) / $dh)) /$dh; - $unit = $units[$d]; - break 1; - } // end if - } // end for - } elseif (!$only_down && (float) $value !== 0.0) { - for ($d = -8; $d <= 8; $d++) { - // force using pow() because of the negative exponent - if (isset($units[$d]) && $value <= $li * PMA_pow(1000, $d-1, 'pow')) { - $value = round($value / (PMA_pow(1000, $d, 'pow') / $dh)) /$dh; - $unit = $units[$d]; - break 1; - } // end if - } // end for - } // end if ($value >= 1) elseif (!$only_down && (float) $value !== 0.0) - - //number_format is not multibyte safe, str_replace is safe - $value = PMA_localizeNumber(number_format($value, $comma)); + $dh = PMA_pow(10, $digits_right); + + // This gives us the right SI prefix already, but $digits_left parameter not incorporated + $d = floor(log10($value) / 3); + // Lowering the SI prefix by 1 gives us an additional 3 zeros + // So if we have 3,6,9,12.. free digits ($digits_left - $cur_digits) to use, then lower the SI prefix + $cur_digits = floor(log10($value / PMA_pow(1000, $d, 'pow'))+1); + if($digits_left > $cur_digits) { + $d-= floor(($digits_left - $cur_digits)/3); + } + + if($d<0 && $only_down) $d=0; + + $value = round($value / (PMA_pow(1000, $d, 'pow') / $dh)) /$dh; + $unit = $units[$d]; + + // If we dont want any zeros after the comma just add the thousand seperator + if($noTrailingZero) + $value = PMA_localizeNumber(preg_replace("/(?<=\d)(?=(\d{3})+(?!\d))/",",",$value)); + else + $value = PMA_localizeNumber(number_format($value, $digits_right)); //number_format is not multibyte safe, str_replace is safe
- if($originalValue!=0 && floatval($value) == 0) return ' <'.(1/PMA_pow(10,$comma)).' '.$unit; + if($originalValue!=0 && floatval($value) == 0) return ' <'.(1/PMA_pow(10,$digits_right)).' '.$unit;
return $sign . $value . ' ' . $unit; } // end of the 'PMA_formatNumber' function diff --git a/libraries/server_links.inc.php b/libraries/server_links.inc.php index 2c1ac6a..e8e2fc9 100644 --- a/libraries/server_links.inc.php +++ b/libraries/server_links.inc.php @@ -50,9 +50,9 @@ if (!$GLOBALS['is_ajax_request']) { $tabs['status']['link'] = 'server_status.php'; $tabs['status']['text'] = __('Status');
- $tabs['process']['icon'] = 's_process.png'; + /*$tabs['process']['icon'] = 's_process.png'; $tabs['process']['link'] = 'server_processlist.php'; - $tabs['process']['text'] = __('Processes'); + $tabs['process']['text'] = __('Processes');*/
if ($is_superuser) { $tabs['rights']['icon'] = 's_rights.png'; diff --git a/server_processlist.php b/server_processlist.php deleted file mode 100644 index 78f0013..0000000 --- a/server_processlist.php +++ /dev/null @@ -1,114 +0,0 @@ -<?php -/* vim: set expandtab sw=4 ts=4 sts=4: */ -/** - * - * @package phpMyAdmin - */ - -/** - * - */ -require_once './libraries/common.inc.php'; -require_once './libraries/server_common.inc.php'; -require './libraries/server_links.inc.php'; - - -/** - * Displays the sub-page heading - */ -echo '<h2>' . "\n" - . ($GLOBALS['cfg']['MainPageIconic'] ? '<img src="' . $pmaThemeImage . 's_process.png" width="16" height="16" border="0" hspace="2" align="middle" alt="" />' : '') - . ' ' . __('Processes') . "\n" - . '</h2>' . "\n"; - -/** - * Kills a selected process - */ -if (!empty($_REQUEST['kill'])) { - if (PMA_DBI_try_query('KILL ' . $_REQUEST['kill'] . ';')) { - $message = PMA_Message::success(__('Thread %s was successfully killed.')); - } else { - $message = PMA_Message::error(__('phpMyAdmin was unable to kill thread %s. It probably has already been closed.')); - } - $message->addParam($_REQUEST['kill']); - $message->display(); -} - -$url_params = array(); - -if (! empty($_REQUEST['full'])) { - $sql_query = 'SHOW FULL PROCESSLIST'; - $url_params['full'] = 1; - $full_text_link = 'server_processlist.php' . PMA_generate_common_url(array(), 'html', '?'); -} else { - $sql_query = 'SHOW PROCESSLIST'; - $full_text_link = 'server_processlist.php' . PMA_generate_common_url(array('full' => 1)); -} -$result = PMA_DBI_query($sql_query); - -/** - * Displays the page - */ -?> -<table id="tableprocesslist" class="data"> -<thead> -<tr> - <?php if (!PMA_DRIZZLE): ?> - <th><a href="<?php echo $full_text_link; ?>" - title="<?php echo empty($full) ? __('Show Full Queries') : __('Truncate Shown Queries'); ?>"> - <img src="<?php echo $pmaThemeImage . 's_' . (empty($_REQUEST['full']) ? 'full' : 'partial'); ?>text.png" - alt="<?php echo empty($_REQUEST['full']) ? __('Show Full Queries') : __('Truncate Shown Queries'); ?>" /> - </a></th> - <?php else: ?> - <th></th> - <?php endif; ?> - <th><?php echo __('ID'); ?></th> - <th><?php echo __('User'); ?></th> - <th><?php echo __('Host'); ?></th> - <th><?php echo __('Database'); ?></th> - <th><?php echo __('Command'); ?></th> - <th><?php echo __('Time'); ?></th> - <th><?php echo __('Status'); ?></th> - <th><?php echo __('SQL query'); ?></th> -</tr> -</thead> -<tbody> -<?php -$odd_row = true; -while($process = PMA_DBI_fetch_assoc($result)) { - if (PMA_DRIZZLE) { - // Drizzle uses uppercase keys - foreach ($process as $k => $v) { - $k = $k !== 'DB' - ? $k = ucfirst(strtolower($k)) - : 'db'; - $process[$k] = $v; - } - } - $url_params['kill'] = $process['Id']; - $kill_process = 'server_processlist.php' . PMA_generate_common_url($url_params); - ?> -<tr class="noclick <?php echo $odd_row ? 'odd' : 'even'; ?>"> - <td><a href="<?php echo $kill_process ; ?>"><?php echo __('Kill'); ?></a></td> - <td class="value"><?php echo $process['Id']; ?></td> - <td><?php echo $process['User']; ?></td> - <td><?php echo $process['Host']; ?></td> - <td><?php echo ((! isset($process['db']) || ! strlen($process['db'])) ? '<i>' . __('None') . '</i>' : $process['db']); ?></td> - <td><?php echo $process['Command']; ?></td> - <td class="value"><?php echo $process['Time']; ?></td> - <td><?php echo (empty($process['State']) ? '---' : $process['State']); ?></td> - <td><?php echo (empty($process['Info']) ? '---' : PMA_SQP_formatHtml(PMA_SQP_parse($process['Info']))); ?></td> -</tr> - <?php - $odd_row = ! $odd_row; -} -?> -</tbody> -</table> -<?php - -/** - * Sends the footer - */ -require './libraries/footer.inc.php'; -?> diff --git a/server_status.php b/server_status.php index 3b44a4a..70bd5d0 100644 --- a/server_status.php +++ b/server_status.php @@ -14,20 +14,45 @@ if (! defined('PMA_NO_VARIABLES_IMPORT')) { define('PMA_NO_VARIABLES_IMPORT', true); } -require_once './libraries/common.inc.php';
-$GLOBALS['js_include'][] = 'pMap.js'; +require_once './libraries/common.inc.php';
-/** - * Does the common work +/** + * Ajax request */ -require './libraries/server_common.inc.php';
+// Prevent ajax requests from being cached +if (isset($_REQUEST['ajax_request'])) { + header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1 + header("Expires: Sat, 26 Jul 1997 05:00:00 GMT"); // Date in the past + header_remove('Last-Modified');
-/** - * Displays the links - */ -require './libraries/server_links.inc.php'; + if (isset($_REQUEST["query_chart"])) { + exit(createQueryChart()); + } + if(isset($_REQUEST['chart_data'])) { + switch($_REQUEST['type']) { + case 'proc': + $c = PMA_DBI_fetch_result('SHOW GLOBAL STATUS WHERE Variable_name="Connections"', 0, 1); + $result = PMA_DBI_query('SHOW PROCESSLIST'); + $num_procs = PMA_DBI_num_rows($result); + + $ret = Array('x'=>(microtime(true)*1000),'y_proc'=>$num_procs,'y_conn'=>$c['Connections']); + exit(json_encode($ret)); + case 'queries': + $queries = PMA_DBI_fetch_result('SHOW GLOBAL STATUS WHERE Variable_name LIKE "Com_%" AND Value>0', 0, 1); + cleanDeprecated($queries); + // admin commands are not queries + unset($queries['Com_admin_commands']); + + $sum=array_sum($queries); + + $ret = Array('x'=>(microtime(true)*1000),'y'=>$sum,'pointInfo'=>$queries,'numQueries'=>count($queries)); + exit(json_encode($ret)); + } + } +} +
/** * Replication library @@ -36,132 +61,18 @@ require './libraries/replication.inc.php'; require_once './libraries/replication_gui.lib.php';
/** - * Chart generation - */ -require_once './libraries/chart.lib.php'; - -/** - * Messages are built using the message name - */ -$strShowStatusBinlog_cache_disk_useDescr = __('The number of transactions that used the temporary binary log cache but that exceeded the value of binlog_cache_size and used a temporary file to store statements from the transaction.'); -$strShowStatusBinlog_cache_useDescr = __('The number of transactions that used the temporary binary log cache.'); -$strShowStatusCreated_tmp_disk_tablesDescr = __('The number of temporary tables on disk created automatically by the server while executing statements. If Created_tmp_disk_tables is big, you may want to increase the tmp_table_size value to cause temporary tables to be memory-based instead of disk-based.'); -$strShowStatusCreated_tmp_filesDescr = __('How many temporary files mysqld has created.'); -$strShowStatusCreated_tmp_tablesDescr = __('The number of in-memory temporary tables created automatically by the server while executing statements.'); -$strShowStatusDelayed_errorsDescr = __('The number of rows written with INSERT DELAYED for which some error occurred (probably duplicate key).'); -$strShowStatusDelayed_insert_threadsDescr = __('The number of INSERT DELAYED handler threads in use. Every different table on which one uses INSERT DELAYED gets its own thread.'); -$strShowStatusDelayed_writesDescr = __('The number of INSERT DELAYED rows written.'); -$strShowStatusFlush_commandsDescr = __('The number of executed FLUSH statements.'); -$strShowStatusHandler_commitDescr = __('The number of internal COMMIT statements.'); -$strShowStatusHandler_deleteDescr = __('The number of times a row was deleted from a table.'); -$strShowStatusHandler_discoverDescr = __('The MySQL server can ask the NDB Cluster storage engine if it knows about a table with a given name. This is called discovery. Handler_discover indicates the number of time tables have been discovered.'); -$strShowStatusHandler_read_firstDescr = __('The number of times the first entry was read from an index. If this is high, it suggests that the server is doing a lot of full index scans; for example, SELECT col1 FROM foo, assuming that col1 is indexed.'); -$strShowStatusHandler_read_keyDescr = __('The number of requests to read a row based on a key. If this is high, it is a good indication that your queries and tables are properly indexed.'); -$strShowStatusHandler_read_nextDescr = __('The number of requests to read the next row in key order. This is incremented if you are querying an index column with a range constraint or if you are doing an index scan.'); -$strShowStatusHandler_read_prevDescr = __('The number of requests to read the previous row in key order. This read method is mainly used to optimize ORDER BY ... DESC.'); -$strShowStatusHandler_read_rndDescr = __('The number of requests to read a row based on a fixed position. This is high if you are doing a lot of queries that require sorting of the result. You probably have a lot of queries that require MySQL to scan whole tables or you have joins that don't use keys properly.'); -$strShowStatusHandler_read_rnd_nextDescr = __('The number of requests to read the next row in the data file. This is high if you are doing a lot of table scans. Generally this suggests that your tables are not properly indexed or that your queries are not written to take advantage of the indexes you have.'); -$strShowStatusHandler_rollbackDescr = __('The number of internal ROLLBACK statements.'); -$strShowStatusHandler_updateDescr = __('The number of requests to update a row in a table.'); -$strShowStatusHandler_writeDescr = __('The number of requests to insert a row in a table.'); -$strShowStatusInnodb_buffer_pool_pages_dataDescr = __('The number of pages containing data (dirty or clean).'); -$strShowStatusInnodb_buffer_pool_pages_dirtyDescr = __('The number of pages currently dirty.'); -$strShowStatusInnodb_buffer_pool_pages_flushedDescr = __('The number of buffer pool pages that have been requested to be flushed.'); -$strShowStatusInnodb_buffer_pool_pages_freeDescr = __('The number of free pages.'); -$strShowStatusInnodb_buffer_pool_pages_latchedDescr = __('The number of latched pages in InnoDB buffer pool. These are pages currently being read or written or that can't be flushed or removed for some other reason.'); -$strShowStatusInnodb_buffer_pool_pages_miscDescr = __('The number of pages busy because they have been allocated for administrative overhead such as row locks or the adaptive hash index. This value can also be calculated as Innodb_buffer_pool_pages_total - Innodb_buffer_pool_pages_free - Innodb_buffer_pool_pages_data.'); -$strShowStatusInnodb_buffer_pool_pages_totalDescr = __('Total size of buffer pool, in pages.'); -$strShowStatusInnodb_buffer_pool_read_ahead_rndDescr = __('The number of "random" read-aheads InnoDB initiated. This happens when a query is to scan a large portion of a table but in random order.'); -$strShowStatusInnodb_buffer_pool_read_ahead_seqDescr = __('The number of sequential read-aheads InnoDB initiated. This happens when InnoDB does a sequential full table scan.'); -$strShowStatusInnodb_buffer_pool_read_requestsDescr = __('The number of logical read requests InnoDB has done.'); -$strShowStatusInnodb_buffer_pool_readsDescr = __('The number of logical reads that InnoDB could not satisfy from buffer pool and had to do a single-page read.'); -$strShowStatusInnodb_buffer_pool_wait_freeDescr = __('Normally, writes to the InnoDB buffer pool happen in the background. However, if it's necessary to read or create a page and no clean pages are available, it's necessary to wait for pages to be flushed first. This counter counts instances of these waits. If the buffer pool size was set properly, this value should be small.'); -$strShowStatusInnodb_buffer_pool_write_requestsDescr = __('The number writes done to the InnoDB buffer pool.'); -$strShowStatusInnodb_data_fsyncsDescr = __('The number of fsync() operations so far.'); -$strShowStatusInnodb_data_pending_fsyncsDescr = __('The current number of pending fsync() operations.'); -$strShowStatusInnodb_data_pending_readsDescr = __('The current number of pending reads.'); -$strShowStatusInnodb_data_pending_writesDescr = __('The current number of pending writes.'); -$strShowStatusInnodb_data_readDescr = __('The amount of data read so far, in bytes.'); -$strShowStatusInnodb_data_readsDescr = __('The total number of data reads.'); -$strShowStatusInnodb_data_writesDescr = __('The total number of data writes.'); -$strShowStatusInnodb_data_writtenDescr = __('The amount of data written so far, in bytes.'); -$strShowStatusInnodb_dblwr_pages_writtenDescr = __('The number of pages that have been written for doublewrite operations.'); -$strShowStatusInnodb_dblwr_writesDescr = __('The number of doublewrite operations that have been performed.'); -$strShowStatusInnodb_log_waitsDescr = __('The number of waits we had because log buffer was too small and we had to wait for it to be flushed before continuing.'); -$strShowStatusInnodb_log_write_requestsDescr = __('The number of log write requests.'); -$strShowStatusInnodb_log_writesDescr = __('The number of physical writes to the log file.'); -$strShowStatusInnodb_os_log_fsyncsDescr = __('The number of fsync() writes done to the log file.'); -$strShowStatusInnodb_os_log_pending_fsyncsDescr = __('The number of pending log file fsyncs.'); -$strShowStatusInnodb_os_log_pending_writesDescr = __('Pending log file writes.'); -$strShowStatusInnodb_os_log_writtenDescr = __('The number of bytes written to the log file.'); -$strShowStatusInnodb_pages_createdDescr = __('The number of pages created.'); -$strShowStatusInnodb_page_sizeDescr = __('The compiled-in InnoDB page size (default 16KB). Many values are counted in pages; the page size allows them to be easily converted to bytes.'); -$strShowStatusInnodb_pages_readDescr = __('The number of pages read.'); -$strShowStatusInnodb_pages_writtenDescr = __('The number of pages written.'); -$strShowStatusInnodb_row_lock_current_waitsDescr = __('The number of row locks currently being waited for.'); -$strShowStatusInnodb_row_lock_time_avgDescr = __('The average time to acquire a row lock, in milliseconds.'); -$strShowStatusInnodb_row_lock_timeDescr = __('The total time spent in acquiring row locks, in milliseconds.'); -$strShowStatusInnodb_row_lock_time_maxDescr = __('The maximum time to acquire a row lock, in milliseconds.'); -$strShowStatusInnodb_row_lock_waitsDescr = __('The number of times a row lock had to be waited for.'); -$strShowStatusInnodb_rows_deletedDescr = __('The number of rows deleted from InnoDB tables.'); -$strShowStatusInnodb_rows_insertedDescr = __('The number of rows inserted in InnoDB tables.'); -$strShowStatusInnodb_rows_readDescr = __('The number of rows read from InnoDB tables.'); -$strShowStatusInnodb_rows_updatedDescr = __('The number of rows updated in InnoDB tables.'); -$strShowStatusKey_blocks_not_flushedDescr = __('The number of key blocks in the key cache that have changed but haven't yet been flushed to disk. It used to be known as Not_flushed_key_blocks.'); -$strShowStatusKey_blocks_unusedDescr = __('The number of unused blocks in the key cache. You can use this value to determine how much of the key cache is in use.'); -$strShowStatusKey_blocks_usedDescr = __('The number of used blocks in the key cache. This value is a high-water mark that indicates the maximum number of blocks that have ever been in use at one time.'); -$strShowStatusKey_read_requestsDescr = __('The number of requests to read a key block from the cache.'); -$strShowStatusKey_readsDescr = __('The number of physical reads of a key block from disk. If Key_reads is big, then your key_buffer_size value is probably too small. The cache miss rate can be calculated as Key_reads/Key_read_requests.'); -$strShowStatusKey_write_requestsDescr = __('The number of requests to write a key block to the cache.'); -$strShowStatusKey_writesDescr = __('The number of physical writes of a key block to disk.'); -$strShowStatusLast_query_costDescr = __('The total cost of the last compiled query as computed by the query optimizer. Useful for comparing the cost of different query plans for the same query. The default value of 0 means that no query has been compiled yet.'); -$strShowStatusNot_flushed_delayed_rowsDescr = __('The number of rows waiting to be written in INSERT DELAYED queues.'); -$strShowStatusOpened_tablesDescr = __('The number of tables that have been opened. If opened tables is big, your table cache value is probably too small.'); -$strShowStatusOpen_filesDescr = __('The number of files that are open.'); -$strShowStatusOpen_streamsDescr = __('The number of streams that are open (used mainly for logging).'); -$strShowStatusOpen_tablesDescr = __('The number of tables that are open.'); -$strShowStatusQcache_free_blocksDescr = __('The number of free memory blocks in query cache.'); -$strShowStatusQcache_free_memoryDescr = __('The amount of free memory for query cache.'); -$strShowStatusQcache_hitsDescr = __('The number of cache hits.'); -$strShowStatusQcache_insertsDescr = __('The number of queries added to the cache.'); -$strShowStatusQcache_lowmem_prunesDescr = __('The number of queries that have been removed from the cache to free up memory for caching new queries. This information can help you tune the query cache size. The query cache uses a least recently used (LRU) strategy to decide which queries to remove from the cache.'); -$strShowStatusQcache_not_cachedDescr = __('The number of non-cached queries (not cachable, or not cached due to the query_cache_type setting).'); -$strShowStatusQcache_queries_in_cacheDescr = __('The number of queries registered in the cache.'); -$strShowStatusQcache_total_blocksDescr = __('The total number of blocks in the query cache.'); -$strShowStatusReset = _pgettext('$strShowStatusReset', 'Reset'); -$strShowStatusRpl_statusDescr = __('The status of failsafe replication (not yet implemented).'); -$strShowStatusSelect_full_joinDescr = __('The number of joins that do not use indexes. If this value is not 0, you should carefully check the indexes of your tables.'); -$strShowStatusSelect_full_range_joinDescr = __('The number of joins that used a range search on a reference table.'); -$strShowStatusSelect_range_checkDescr = __('The number of joins without keys that check for key usage after each row. (If this is not 0, you should carefully check the indexes of your tables.)'); -$strShowStatusSelect_rangeDescr = __('The number of joins that used ranges on the first table. (It's normally not critical even if this is big.)'); -$strShowStatusSelect_scanDescr = __('The number of joins that did a full scan of the first table.'); -$strShowStatusSlave_open_temp_tablesDescr = __('The number of temporary tables currently open by the slave SQL thread.'); -$strShowStatusSlave_retried_transactionsDescr = __('Total (since startup) number of times the replication slave SQL thread has retried transactions.'); -$strShowStatusSlave_runningDescr = __('This is ON if this server is a slave that is connected to a master.'); -$strShowStatusSlow_launch_threadsDescr = __('The number of threads that have taken more than slow_launch_time seconds to create.'); -$strShowStatusSlow_queriesDescr = __('The number of queries that have taken more than long_query_time seconds.'); -$strShowStatusSort_merge_passesDescr = __('The number of merge passes the sort algorithm has had to do. If this value is large, you should consider increasing the value of the sort_buffer_size system variable.'); -$strShowStatusSort_rangeDescr = __('The number of sorts that were done with ranges.'); -$strShowStatusSort_rowsDescr = __('The number of sorted rows.'); -$strShowStatusSort_scanDescr = __('The number of sorts that were done by scanning the table.'); -$strShowStatusTable_locks_immediateDescr = __('The number of times that a table lock was acquired immediately.'); -$strShowStatusTable_locks_waitedDescr = __('The number of times that a table lock could not be acquired immediately and a wait was needed. If this is high, and you have performance problems, you should first optimize your queries, and then either split your table or tables or use replication.'); -$strShowStatusThreads_cachedDescr = __('The number of threads in the thread cache. The cache hit rate can be calculated as Threads_created/Connections. If this value is red you should raise your thread_cache_size.'); -$strShowStatusThreads_connectedDescr = __('The number of currently open connections.'); -$strShowStatusThreads_createdDescr = __('The number of threads created to handle connections. If Threads_created is big, you may want to increase the thread_cache_size value. (Normally this doesn't give a notable performance improvement if you have a good thread implementation.)'); -$strShowStatusThreads_runningDescr = __('The number of threads that are not sleeping.'); - -/** - * Displays the sub-page heading + * JS Includes */ -echo '<div id="serverstatus">' . "\n"; -echo '<h2>' . "\n" - . ($GLOBALS['cfg']['MainPageIconic'] - ? '<img class="icon" src="' . $GLOBALS['pmaThemeImage'] . - 's_status.png" width="16" height="16" alt="" />' - : '') - . __('Runtime Information') . "\n" - . '</h2>' . "\n"; + +$GLOBALS['js_include'][] = 'server_status.js'; +$GLOBALS['js_include'][] = 'jquery/jquery-ui-1.8.custom.js'; +$GLOBALS['js_include'][] = 'jquery/jquery.tablesorter.js'; +$GLOBALS['js_include'][] = 'jquery/jquery.cookie.js'; // For tab persistence +$GLOBALS['js_include'][] = 'highcharts/highcharts.js'; +/* Files required for chart exporting */ +$GLOBALS['js_include'][] = 'highcharts/exporting.js'; +$GLOBALS['js_include'][] = 'canvg/canvg.js'; +$GLOBALS['js_include'][] = 'canvg/rgbcolor.js';
/** @@ -180,6 +91,20 @@ if (isset($_REQUEST['flush'])) { unset($_flush_commands); }
+/** + * Kills a selected process + */ +if (!empty($_REQUEST['kill'])) { + if (PMA_DBI_try_query('KILL ' . $_REQUEST['kill'] . ';')) { + $message = PMA_Message::success(__('Thread %s was successfully killed.')); + } else { + $message = PMA_Message::error(__('phpMyAdmin was unable to kill thread %s. It probably has already been closed.')); + } + $message->addParam($_REQUEST['kill']); + //$message->display(); +} + +
/** * get status from server @@ -192,29 +117,9 @@ $server_status = PMA_DBI_fetch_result('SHOW GLOBAL STATUS', 0, 1); $server_variables = PMA_DBI_fetch_result('SHOW GLOBAL VARIABLES', 0, 1);
/** - * starttime calculation + * cleanup of some deprecated values */ -$start_time = PMA_DBI_fetch_value( - 'SELECT UNIX_TIMESTAMP() - ' . $server_status['Uptime']); - - -/** - * cleanup some deprecated values - */ -$deprecated = array( - 'Com_prepare_sql' => 'Com_stmt_prepare', - 'Com_execute_sql' => 'Com_stmt_execute', - 'Com_dealloc_sql' => 'Com_stmt_close', -); - -foreach ($deprecated as $old => $new) { - if (isset($server_status[$old]) - && isset($server_status[$new])) { - unset($server_status[$old]); - } -} -unset($deprecated); - +cleanDeprecated($server_status);
/** * calculate some values @@ -266,64 +171,12 @@ if (isset($server_status['Uptime_since_flush_status'])) { }
/** - * define some alerts - */ -// name => max value before alert -$alerts = array( - // lower is better - // variable => max value - 'Aborted_clients' => 0, - 'Aborted_connects' => 0, - - 'Binlog_cache_disk_use' => 0, - - 'Created_tmp_disk_tables' => 0, - - 'Handler_read_rnd' => 0, - 'Handler_read_rnd_next' => 0, - - 'Innodb_buffer_pool_pages_dirty' => 0, - 'Innodb_buffer_pool_reads' => 0, - 'Innodb_buffer_pool_wait_free' => 0, - 'Innodb_log_waits' => 0, - 'Innodb_row_lock_time_avg' => 10, // ms - 'Innodb_row_lock_time_max' => 50, // ms - 'Innodb_row_lock_waits' => 0, - - 'Slow_queries' => 0, - 'Delayed_errors' => 0, - 'Select_full_join' => 0, - 'Select_range_check' => 0, - 'Sort_merge_passes' => 0, - 'Opened_tables' => 0, - 'Table_locks_waited' => 0, - 'Qcache_lowmem_prunes' => 0, - 'Slow_launch_threads' => 0, - - // depends on Key_read_requests - // normaly lower then 1:0.01 - 'Key_reads' => (0.01 * $server_status['Key_read_requests']), - // depends on Key_write_requests - // normaly nearly 1:1 - 'Key_writes' => (0.9 * $server_status['Key_write_requests']), - - 'Key_buffer_fraction' => 0.5, - - // alert if more than 95% of thread cache is in use - 'Threads_cached' => 0.95 * $server_variables['thread_cache_size'] - - // higher is better - // variable => min value - //'Handler read key' => '> ', -); - - -/** * split variables in sections */ $allocations = array( // variable name => section - + // variable names match when they begin with the given string + 'Com_' => 'com', 'Innodb_' => 'innodb', 'Ndb_' => 'ndb', @@ -368,24 +221,24 @@ $allocations = array(
$sections = array( // section => section name (description) - 'com' => array('title' => ''), - 'query' => array('title' => __('SQL query')), - 'innodb' => array('title' => 'InnoDB'), - 'ndb' => array('title' => 'NDB'), - 'handler' => array('title' => __('Handler')), - 'qcache' => array('title' => __('Query cache')), - 'threads' => array('title' => __('Threads')), - 'binlog_cache' => array('title' => __('Binary log')), - 'created_tmp' => array('title' => __('Temporary data')), - 'delayed' => array('title' => __('Delayed inserts')), - 'key' => array('title' => __('Key cache')), - 'select' => array('title' => __('Joins')), - 'repl' => array('title' => __('Replication')), - 'sort' => array('title' => __('Sorting')), - 'table' => array('title' => __('Tables')), - 'tc' => array('title' => __('Transaction coordinator')), - 'files' => array('title' => __('Files')), - 'ssl' => array('title' => 'SSL'), + 'com' => 'Com', + 'query' => __('SQL query'), + 'innodb' => 'InnoDB', + 'ndb' => 'NDB', + 'handler' => __('Handler'), + 'qcache' => __('Query cache'), + 'threads' => __('Threads'), + 'binlog_cache' => __('Binary log'), + 'created_tmp' => __('Temporary data'), + 'delayed' => __('Delayed inserts'), + 'key' => __('Key cache'), + 'select' => __('Joins'), + 'repl' => __('Replication'), + 'sort' => __('Sorting'), + 'table' => __('Tables'), + 'tc' => __('Transaction coordinator'), + 'files' => __('Files'), + 'ssl' => 'SSL', );
/** @@ -417,8 +270,8 @@ $links['qcache'][__('Flush query cache')] PMA_generate_common_url(); $links['qcache']['doc'] = 'query_cache';
-$links['threads'][__('Show processes')] - = 'server_processlist.php?' . PMA_generate_common_url(); +//$links['threads'][__('Show processes')] +// = 'server_processlist.php?' . PMA_generate_common_url(); $links['threads']['doc'] = 'mysql_threads';
$links['key']['doc'] = 'myisam_key_cache'; @@ -435,337 +288,717 @@ $links['innodb'][__('InnoDB Status')] $links['innodb']['doc'] = 'innodb';
-// sort status vars into arrays +// Variable to contain all com_ variables +$used_queries = Array(); + +// Variable to map variable names to their respective section name (used for js category filtering) +$allocationMap = Array(); + +// sort vars into arrays foreach ($server_status as $name => $value) { - if (isset($allocations[$name])) { - $sections[$allocations[$name]]['vars'][$name] = $value; - unset($server_status[$name]); - } else { - foreach ($allocations as $filter => $section) { - if (preg_match('/^' . $filter . '/', $name) - && isset($server_status[$name])) { - unset($server_status[$name]); - $sections[$section]['vars'][$name] = $value; - } + foreach ($allocations as $filter => $section) { + if (strpos($name, $filter) !== FALSE) { + $allocationMap[$name] = $section; + if($section=='com' && $value>0) $used_queries[$name] = $value; + break; // Only exits inner loop } } } -unset($name, $value, $filter, $section, $allocations); - -// rest -$sections['all']['vars'] =& $server_status;
-$hour_factor = 3600 / $server_status['Uptime']; +// admin commands are not queries (e.g. they include COM_PING, which is excluded from $server_status['Questions']) +unset($used_queries['Com_admin_commands']); + +/* Ajax request refresh */ +if(isset($_REQUEST['show']) && isset($_REQUEST['ajax_request'])) { + switch($_REQUEST['show']) { + case 'query_statistics': + printQueryStatistics(); + exit(); + case 'server_traffic': + printServerTraffic(); + exit(); + case 'variables_table': + // Prints the variables table + printVariablesTable(); + exit(); + + default: + break; + } +}
/** * start output */ + + /** + * Does the common work + */ +require './libraries/server_common.inc.php'; + + +/** + * Displays the links + */ +require './libraries/server_links.inc.php'; + ?> -<div id="statuslinks"> - <a href="<?php echo - $PMA_PHP_SELF . '?' . PMA_generate_common_url(); ?>" - ><?php echo __('Refresh'); ?></a> - <a href="<?php echo - $PMA_PHP_SELF . '?flush=STATUS&' . PMA_generate_common_url(); ?>" - ><?php echo _pgettext('for Show status', 'Reset'); ?></a> - <?php echo PMA_showMySQLDocu('server_status_variables','server_status_variables'); ?> +<script type="text/javascript"> +pma_token = '<?php echo $_SESSION[' PMA_token ']; ?>'; +url_query = '<?php echo $url_query;?>'; +pma_theme_image = '<?php echo $GLOBALS['pmaThemeImage']; ?>'; +</script> +<div id="serverstatus"> + <h2><? + +/** + * Displays the sub-page heading + */ +if($GLOBALS['cfg']['MainPageIconic']) + echo '<img class="icon" src="' . $GLOBALS['pmaThemeImage'] . 's_status.png" width="16" height="16" alt="" />'; + +echo __('Runtime Information'); + +?></h2> + <div id="serverStatusTabs"> + <ul> + <li><a href="#statustabs_traffic"><?php echo __('Server traffic'); ?></a></li> + <li><a href="#statustabs_queries"><?php echo __('Query statistics'); ?></a></li> + <li><a href="#statustabs_allvars"><?php echo __('All status variables'); ?></a></li> + </ul> + + <div id="statustabs_traffic"> + <div class="statuslinks"> + <a class="tabRefresh" href="<?php echo $PMA_PHP_SELF . '?show=server_traffic&' . PMA_generate_common_url(); ?>" > + <img src="<?php echo $GLOBALS['pmaThemeImage'];?>ajax_clock_small.gif" alt="ajax clock" style="display: none;" /> + <?php echo __('Refresh'); ?> + </a> + <select name="trafficChartRefresh" style="display:none;"> + <option value="5"><?php echo __('Refresh rate'); ?></option> + <option value="1">1 <?php echo __('second'); ?></option> + <option value="2">2 <?php echo __('seconds'); ?></option> + <option value="5">5 <?php echo __('seconds'); ?></option> + <option value="10">10 <?php echo __('seconds'); ?></option> + <option value="20">20 <?php echo __('seconds'); ?></option> + <option value="40">40 <?php echo __('seconds'); ?></option> + <option value="60">1 <?php echo __('minutes'); ?></option> + <option value="120">2 <?php echo __('minutes'); ?></option> + <option value="300">5 <?php echo __('minutes'); ?></option> + <option value="600">10 <?php echo __('minutes'); ?></option> + </select> + + <a class="tabChart" href="#"> + <?php echo __('Realtime chart'); ?> + </a> + </div> + <div class="tabInnerContent"> + <?php printServerTraffic(); ?> + </div> + </div> + <div id="statustabs_queries"> + <div class="statuslinks"> + <a class="tabRefresh" href="<?php echo $PMA_PHP_SELF . '?show=query_statistics&' . PMA_generate_common_url(); ?>" > + <img src="<?php echo $GLOBALS['pmaThemeImage'];?>ajax_clock_small.gif" alt="ajax clock" style="display: none;" /> + <?php echo __('Refresh'); ?> + </a> + <select name="queryChartRefresh" style="display:none;"> + <option value="5"><?php echo __('Refresh rate'); ?></option> + <option value="1">1 <?php echo __('second'); ?></option> + <option value="2">2 <?php echo __('seconds'); ?></option> + <option value="5">5 <?php echo __('seconds'); ?></option> + <option value="10">10 <?php echo __('seconds'); ?></option> + <option value="20">20 <?php echo __('seconds'); ?></option> + <option value="40">40 <?php echo __('seconds'); ?></option> + <option value="60">1 <?php echo __('minutes'); ?></option> + <option value="120">2 <?php echo __('minutes'); ?></option> + <option value="300">5 <?php echo __('minutes'); ?></option> + <option value="600">10 <?php echo __('minutes'); ?></option> + </select> + <a class="tabChart" href="#"> + <?php echo __('Realtime chart'); ?> + </a> + </div> + <div class="tabInnerContent"> + <?php printQueryStatistics(); ?> + </div> + </div> + <div id="statustabs_allvars"> + <fieldset id="tableFilter"> + <div class="statuslinks"> + <a class="tabRefresh" href="<?php echo $PMA_PHP_SELF . '?show=variables_table&' . PMA_generate_common_url(); ?>" > + <img src="<?php echo $GLOBALS['pmaThemeImage'];?>ajax_clock_small.gif" alt="ajax clock" style="display: none;" /> + <?php echo __('Refresh'); ?> + </a> + </div> + <legend>Filters</legend> + <div class="formelement"> + <label for="filterText"><?php echo __('Containing the word:'); ?></label> + <input name="filterText" type="text" id="filterText" style="vertical-align: baseline;" /> + </div> + <div class="formelement"> + <input type="checkbox" name="filterAlert" id="filterAlert"> + <label for="filterAlert"><?php echo __('Show only alert values'); ?></label> + </div> + <div class="formelement"> + <select id="filterCategory" name="filterCategory"> + <option value=''><?php echo __('Filter by category...'); ?></option> + <?php + foreach($sections as $section_id=>$section_name) { + ?> + <option value='<?php echo $section_id; ?>'><?php echo $section_name; ?></option> + <?php + } + + ?> + </select> + </div> + </fieldset> + <div id="linkSuggestions" class="defaultLinks" style="display:none"> + <p><?php echo __('Related links:'); ?> + <?php + foreach ($links as $section_name => $section_links) { + echo '<span class="status_'.$section_name.'"> '; + $i=0; + foreach ($section_links as $link_name => $link_url) { + if($i>0) echo ', '; + if ('doc' == $link_name) { + echo PMA_showMySQLDocu($link_url, $link_url); + } else { + echo '<a href="' . $link_url . '">' . $link_name . '</a>'; + } + $i++; + } + echo '</span>'; + } + unset($link_url, $link_name, $i); + ?> + </p> + </div> + <div class="tabInnerContent"> + <?php printVariablesTable(); ?> + </div> + </div> + </div> </div>
-<p> <?php -echo sprintf(__('This MySQL server has been running for %s. It started up on %s.'), - PMA_timespanFormat($server_status['Uptime']), - PMA_localisedDate($start_time)) . "\n"; -?> -</p>
-<?php -if ($server_master_status || $server_slave_status) { - echo '<p>'; - if ($server_master_status && $server_slave_status) { - echo __('This MySQL server works as <b>master</b> and <b>slave</b> in <b>replication</b> process.'); - } elseif ($server_master_status) { - echo __('This MySQL server works as <b>master</b> in <b>replication</b> process.'); - } elseif ($server_slave_status) { - echo __('This MySQL server works as <b>slave</b> in <b>replication</b> process.'); +function printQueryStatistics() { + global $server_status, $used_queries, $url_query, $PMA_PHP_SELF; + + $hour_factor = 3600 / $server_status['Uptime']; + + $total_queries = array_sum($used_queries); + + ?> + <h3 id="serverstatusqueries"><?php echo + //sprintf(__('<b>Query statistics</b>: Since its startup, %s queries have been sent to the server.'), + //PMA_formatNumber($server_status['Questions'], 0)); + sprintf('Queries since startup: %s',PMA_formatNumber($total_queries, 0)); + //echo PMA_showMySQLDocu('server-status-variables', 'server-status-variables', false, 'statvar_Questions'); + ?> + <br> + <span style="font-size:60%; display:inline;"> + ø <?php echo __('per hour'); ?>: + <?php echo PMA_formatNumber($total_queries * $hour_factor, 0); ?><br> + + ø <?php echo __('per minute'); ?>: + <?php echo PMA_formatNumber( $total_queries * 60 / $server_status['Uptime'], 0); ?><br> + + <?php if($total_queries / $server_status['Uptime'] >= 1) { + ?> + ø <?php echo __('per second'); ?>: + <?php echo PMA_formatNumber( $total_queries / $server_status['Uptime'], 0); ?><br> + + <?php } - echo __('For further information about replication status on the server, please visit the <a href=#replication>replication section</a>.'); - echo '</p>'; -} -?>
-<div id="sectionlinks"> -<?php -foreach ($sections as $section_name => $section) { - if (! empty($section['vars']) && ! empty($section['title'])) { - echo '<a href="' . $PMA_PHP_SELF . '?' . - PMA_generate_common_url() . '#' . $section_name . '">' . - $section['title'] . '</a>' . "\n"; + // reverse sort by value to show most used statements first + arsort($used_queries); + + $odd_row = true; + $count_displayed_rows = 0; + $perc_factor = 100 / $total_queries //(- $server_status['Connections']); + + ?> + </h3> + <table id="serverstatusqueriesdetails" class="data sortable"> + <col class="namecol" /> + <col class="valuecol" span="3" /> + <thead> + <tr><th><?php echo __('Query type'); ?></th> + <th><?php + /* l10n: # = Amount of queries */ + echo __('#'); + ?> + <th>ø <?php echo __('per hour'); ?></th> + <th>%</th> + </tr> + </thead> + <tbody> + + <?php + $chart_json = Array(); + $query_sum = array_sum($used_queries); + $other_sum = 0; + foreach ($used_queries as $name => $value) { + $odd_row = !$odd_row; + + // For the percentage column, use Questions - Connections, because + // the number of connections is not an item of the Query types + // but is included in Questions. Then the total of the percentages is 100. + $name = str_replace(Array('Com_','_'), Array('',' '), $name); + + if($value < $query_sum * 0.02) + $other_sum += $value; + else $chart_json[$name] = $value; + ?> + <tr class="noclick <?php echo $odd_row ? 'odd' : 'even'; ?>"> + <th class="name"><?php echo htmlspecialchars($name); ?></th> + <td class="value"><?php echo PMA_formatNumber($value, 5, 0, true); ?></td> + <td class="value"><?php echo + PMA_formatNumber($value * $hour_factor, 4, 1, true); ?></td> + <td class="value"><?php echo + PMA_formatNumber($value * $perc_factor, 0, 2); ?>%</td> + </tr> + <?php } + ?> + </tbody> + </table> + + <div id="serverstatusquerieschart" style="width:500px; height:350px; "> + <?php + /*// Generate the graph if this is an ajax request + if(isset($_REQUEST['ajax_request'])) { + echo createQueryChart(); + } else { + echo '<a href="'.$PMA_PHP_SELF.'?'.$url_query.'&query_chart=1#serverstatusqueries"' + .'title="' . __('Show query chart') . '">['.__('Show query chart').']</a>'; + }*/ + + if($other_sum>0) + $chart_json[__('Other')] = $other_sum; + + echo json_encode($chart_json); + + ?> + </div> + <?php } -?> -</div> - -<h3><?php echo __('<b>Server traffic</b>: These tables show the network traffic statistics of this MySQL server since its startup.'); ?></h3> - -<table id="serverstatustraffic" class="data"> -<thead> -<tr> - <th colspan="2"><?php echo __('Traffic') . ' ' . PMA_showHint(__('On a busy server, the byte counters may overrun, so those statistics as reported by the MySQL server may be incorrect.')); ?></th> - <th>ø <?php echo __('per hour'); ?></th> -</tr> -</thead> -<tbody> -<tr class="noclick odd"> - <th class="name"><?php echo __('Received'); ?></th> - <td class="value"><?php echo - implode(' ', - PMA_formatByteDown($server_status['Bytes_received'], 2, 1)); ?></td> - <td class="value"><?php echo - implode(' ', - PMA_formatByteDown( - $server_status['Bytes_received'] * $hour_factor, 2, 1)); ?></td> -</tr> -<tr class="noclick even"> - <th class="name"><?php echo __('Sent'); ?></th> - <td class="value"><?php echo - implode(' ', - PMA_formatByteDown($server_status['Bytes_sent'], 2, 1)); ?></td> - <td class="value"><?php echo - implode(' ', - PMA_formatByteDown( - $server_status['Bytes_sent'] * $hour_factor, 2, 1)); ?></td> -</tr> -<tr class="noclick odd"> - <th class="name"><?php echo __('Total'); ?></th> - <td class="value"><?php echo - implode(' ', - PMA_formatByteDown( - $server_status['Bytes_received'] + $server_status['Bytes_sent'], 2, 1) - ); ?></td> - <td class="value"><?php echo - implode(' ', - PMA_formatByteDown( - ($server_status['Bytes_received'] + $server_status['Bytes_sent']) - * $hour_factor, 2, 1) - ); ?></td> -</tr> -</tbody> -</table> - -<table id="serverstatusconnections" class="data"> -<thead> -<tr> - <th colspan="2"><?php echo __('Connections'); ?></th> - <th>ø <?php echo __('per hour'); ?></th> - <th>%</th> -</tr> -</thead> -<tbody> -<tr class="noclick odd"> - <th class="name"><?php echo __('max. concurrent connections'); ?></th> - <td class="value"><?php echo - PMA_formatNumber($server_status['Max_used_connections'], 0); ?> </td> - <td class="value">--- </td> - <td class="value">--- </td> -</tr> -<tr class="noclick even"> - <th class="name"><?php echo __('Failed attempts'); ?></th> - <td class="value"><?php echo - PMA_formatNumber($server_status['Aborted_connects'], 4, 0); ?></td> - <td class="value"><?php echo - PMA_formatNumber($server_status['Aborted_connects'] * $hour_factor, - 4, 2); ?></td> - <td class="value"><?php echo - $server_status['Connections'] > 0 - ? PMA_formatNumber( - $server_status['Aborted_connects'] * 100 / $server_status['Connections'], - 0, 2) . '%' - : '--- '; ?></td> -</tr> -<tr class="noclick odd"> - <th class="name"><?php echo __('Aborted'); ?></th> - <td class="value"><?php echo - PMA_formatNumber($server_status['Aborted_clients'], 4, 0); ?></td> - <td class="value"><?php echo - PMA_formatNumber($server_status['Aborted_clients'] * $hour_factor, - 4, 2); ?></td> - <td class="value"><?php echo - $server_status['Connections'] > 0 - ? PMA_formatNumber( - $server_status['Aborted_clients'] * 100 / $server_status['Connections'], - 0, 2) . '%' - : '--- '; ?></td> -</tr> -<tr class="noclick even"> - <th class="name"><?php echo __('Total'); ?></th> - <td class="value"><?php echo - PMA_formatNumber($server_status['Connections'], 4, 0); ?></td> - <td class="value"><?php echo - PMA_formatNumber($server_status['Connections'] * $hour_factor, - 4, 2); ?></td> - <td class="value"><?php echo - PMA_formatNumber(100, 0, 2); ?>%</td> -</tr> -</tbody> -</table> - -<hr class="clearfloat" /> - -<h3 id="serverstatusqueries"><?php echo - sprintf(__('<b>Query statistics</b>: Since its startup, %s queries have been sent to the server.'), - PMA_formatNumber($server_status['Questions'], 0)); - echo PMA_showMySQLDocu('server-status-variables', 'server-status-variables', false, 'statvar_Questions'); - ?></h3> - -<table id="serverstatusqueriessummary" class="data"> -<thead> -<tr> - <th><?php echo __('Total'); ?></th> - <th>ø <?php echo __('per hour'); ?></th> - <th>ø <?php echo __('per minute'); ?></th> - <th>ø <?php echo __('per second'); ?></th> -</tr> -</thead> -<tbody> -<tr class="noclick odd"> - <td class="value"><?php echo - PMA_formatNumber($server_status['Questions'], 4, 0); ?></td> - <td class="value"><?php echo - PMA_formatNumber($server_status['Questions'] * $hour_factor, - 3, 2); ?></td> - <td class="value"><?php echo - PMA_formatNumber( - $server_status['Questions'] * 60 / $server_status['Uptime'], - 3, 2); ?></td> - <td class="value"><?php echo - PMA_formatNumber( - $server_status['Questions'] / $server_status['Uptime'], - 3, 2); ?></td> -</tr> -</tbody> -</table> - -<div id="serverstatusqueriesdetails"> -<?php
-$used_queries = $sections['com']['vars']; -// reverse sort by value to show most used statements first -arsort($used_queries); -// remove all zero values from the end -// variable empty for Drizzle -if ($used_queries) { - while (end($used_queries) == 0) { - array_pop($used_queries); +function printServerTraffic() { + global $server_status,$PMA_PHP_SELF; + global $server_master_status, $server_slave_status; + + $hour_factor = 3600 / $server_status['Uptime']; + + /** + * starttime calculation + */ + $start_time = PMA_DBI_fetch_value( + 'SELECT UNIX_TIMESTAMP() - ' . $server_status['Uptime']); + + ?> + <h3><?php /* echo __('<b>Server traffic</b>: These tables show the network traffic statistics of this MySQL server since its startup.');*/ + echo sprintf(__('Network traffic since startup: %s'), + implode(' ', PMA_formatByteDown( $server_status['Bytes_received'] + $server_status['Bytes_sent'], 3, 1)) + ); + ?> + </h3> + + <p> + <?php + echo sprintf(__('This MySQL server has been running for %s. It started up on %s.'), + PMA_timespanFormat($server_status['Uptime']), + PMA_localisedDate($start_time)) . "\n"; + ?> + </p> + + <?php + if ($server_master_status || $server_slave_status) { + echo '<p>'; + if ($server_master_status && $server_slave_status) { + echo __('This MySQL server works as <b>master</b> and <b>slave</b> in <b>replication</b> process.'); + } elseif ($server_master_status) { + echo __('This MySQL server works as <b>master</b> in <b>replication</b> process.'); + } elseif ($server_slave_status) { + echo __('This MySQL server works as <b>slave</b> in <b>replication</b> process.'); + } + echo __('For further information about replication status on the server, please visit the <a href=#replication>replication section</a>.'); + echo '</p>'; } -}
-// number of tables to split values into -$tables = 3; -$max_rows_per_table = (int) ceil(count($used_queries) / $tables); -$current_table = 0; -$odd_row = true; -$count_displayed_rows = 0; -$perc_factor = 100 / ($server_status['Questions'] - $server_status['Connections']); - -foreach ($used_queries as $name => $value) { - $current_table++; - if ($count_displayed_rows === 0 || $count_displayed_rows === $max_rows_per_table) { - $odd_row = true; - if ($count_displayed_rows === $max_rows_per_table) { - echo ' </tbody>' . "\n"; - echo ' </table>' . "\n"; - $count_displayed_rows = 0; + /* if the server works as master or slave in replication process, display useful information */ + if ($server_master_status || $server_slave_status) + { + ?> + <hr class="clearfloat" /> + + <h3><a name="replication"></a><?php echo __('Replication status'); ?></h3> + <?php + + foreach ($replication_types as $type) + { + if (${"server_{$type}_status"}) { + PMA_replication_print_status_table($type); + } } -?> - <table id="serverstatusqueriesdetails<?php echo $current_table; ?>" class="data"> - <col class="namecol" /> - <col class="valuecol" span="3" /> + unset($types); + } + ?> + + <table id="serverstatustraffic" class="data"> <thead> - <tr><th colspan="2"><?php echo __('Query type'); ?></th> - <th>ø <?php echo __('per hour'); ?></th> - <th>%</th> - </tr> + <tr> + <th colspan="2"><?php echo __('Traffic') . ' ' . PMA_showHint(__('On a busy server, the byte counters may overrun, so those statistics as reported by the MySQL server may be incorrect.')); ?></th> + <th>ø <?php echo __('per hour'); ?></th> + </tr> </thead> <tbody> -<?php - } else { - $odd_row = !$odd_row; - } - $count_displayed_rows++; + <tr class="noclick odd"> + <th class="name"><?php echo __('Received'); ?></th> + <td class="value"><?php echo + implode(' ', + PMA_formatByteDown($server_status['Bytes_received'], 3, 1)); ?></td> + <td class="value"><?php echo + implode(' ', + PMA_formatByteDown( + $server_status['Bytes_received'] * $hour_factor, 3, 1)); ?></td> + </tr> + <tr class="noclick even"> + <th class="name"><?php echo __('Sent'); ?></th> + <td class="value"><?php echo + implode(' ', + PMA_formatByteDown($server_status['Bytes_sent'], 3, 1)); ?></td> + <td class="value"><?php echo + implode(' ', + PMA_formatByteDown( + $server_status['Bytes_sent'] * $hour_factor, 3, 1)); ?></td> + </tr> + <tr class="noclick odd"> + <th class="name"><?php echo __('Total'); ?></th> + <td class="value"><?php echo + implode(' ', + PMA_formatByteDown( + $server_status['Bytes_received'] + $server_status['Bytes_sent'], 3, 1) + ); ?></td> + <td class="value"><?php echo + implode(' ', + PMA_formatByteDown( + ($server_status['Bytes_received'] + $server_status['Bytes_sent']) + * $hour_factor, 3, 1) + ); ?></td> + </tr> + </tbody> + </table>
-// For the percentage column, use Questions - Connections, because -// the number of connections is not an item of the Query types -// but is included in Questions. Then the total of the percentages is 100. - $name = str_replace('Com_', '', $name); - $name = str_replace('_', ' ', $name); -?> - <tr class="noclick <?php echo $odd_row ? 'odd' : 'even'; ?>"> - <th class="name"><?php echo htmlspecialchars($name); ?></th> - <td class="value"><?php echo PMA_formatNumber($value, 4, 0); ?></td> - <td class="value"><?php echo - PMA_formatNumber($value * $hour_factor, 3, 3); ?></td> - <td class="value"><?php echo - PMA_formatNumber($value * $perc_factor, 0, 2); ?>%</td> - </tr> -<?php -} -?> + <table id="serverstatusconnections" class="data"> + <thead> + <tr> + <th colspan="2"><?php echo __('Connections'); ?></th> + <th>ø <?php echo __('per hour'); ?></th> + <th>%</th> + </tr> + </thead> + <tbody> + <tr class="noclick odd"> + <th class="name"><?php echo __('max. concurrent connections'); ?></th> + <td class="value"><?php echo + PMA_formatNumber($server_status['Max_used_connections'], 0); ?> </td> + <td class="value">--- </td> + <td class="value">--- </td> + </tr> + <tr class="noclick even"> + <th class="name"><?php echo __('Failed attempts'); ?></th> + <td class="value"><?php echo + PMA_formatNumber($server_status['Aborted_connects'], 4, 1, true); ?></td> + <td class="value"><?php echo + PMA_formatNumber($server_status['Aborted_connects'] * $hour_factor, + 4, 2, true); ?></td> + <td class="value"><?php echo + $server_status['Connections'] > 0 + ? PMA_formatNumber( + $server_status['Aborted_connects'] * 100 / $server_status['Connections'], + 0, 2, true) . '%' + : '--- '; ?></td> + </tr> + <tr class="noclick odd"> + <th class="name"><?php echo __('Aborted'); ?></th> + <td class="value"><?php echo + PMA_formatNumber($server_status['Aborted_clients'], 4, 1, true); ?></td> + <td class="value"><?php echo + PMA_formatNumber($server_status['Aborted_clients'] * $hour_factor, + 4, 2, true); ?></td> + <td class="value"><?php echo + $server_status['Connections'] > 0 + ? PMA_formatNumber( + $server_status['Aborted_clients'] * 100 / $server_status['Connections'], + 0, 2, true) . '%' + : '--- '; ?></td> + </tr> + <tr class="noclick even"> + <th class="name"><?php echo __('Total'); ?></th> + <td class="value"><?php echo + PMA_formatNumber($server_status['Connections'], 4, 0); ?></td> + <td class="value"><?php echo + PMA_formatNumber($server_status['Connections'] * $hour_factor, + 4, 2); ?></td> + <td class="value"><?php echo + PMA_formatNumber(100, 0, 2); ?>%</td> + </tr> </tbody> </table> - <div class="clearfloat"></div> -</div> + <?
-<?php if ($used_queries): ?> -<div id="serverstatusquerieschart"> -<?php - if (empty($_REQUEST["query_chart"])) { - echo '<a href="' . $PMA_PHP_SELF . '?' . $url_query - . '&query_chart=1#serverstatusqueries"' - . 'title="' . __('Show query chart') . '">[' - . __('Show query chart') . ']</a>'; - PMA_Message::notice( __('Note: Generating the query chart can take a long time.'))->display(); - } else { - echo PMA_chart_status($used_queries); - } -?> -</div> -<?php endif; ?> + $url_params = array();
-<div id="serverstatussection"> -<?php -//Unset used variables -unset( - $tables, $max_rows_per_table, $current_table, $count_displayed_rows, $perc_factor, - $hour_factor, $sections['com'], - $server_status['Aborted_clients'], $server_status['Aborted_connects'], - $server_status['Max_used_connections'], $server_status['Bytes_received'], - $server_status['Bytes_sent'], $server_status['Connections'], - $server_status['Questions'], $server_status['Uptime'], - $used_queries -); + if (! empty($_REQUEST['full'])) { + $sql_query = 'SHOW FULL PROCESSLIST'; + $url_params['full'] = 1; + $full_text_link = 'server_status.php' . PMA_generate_common_url(array(), 'html', '?'); + } else { + $sql_query = 'SHOW PROCESSLIST'; + $full_text_link = 'server_status.php' . PMA_generate_common_url(array('full' => 1)); + } + $result = PMA_DBI_query($sql_query);
-foreach ($sections as $section_name => $section) { - if (! empty($section['vars'])) { -?> - <table class="data" id="serverstatussection<?php echo $section_name; ?>"> - <caption class="tblHeaders"> - <a class="top" - href="<?php echo $PMA_PHP_SELF . '?' . - PMA_generate_common_url() . '#serverstatus'; ?>" - name="<?php echo $section_name; ?>"><?php echo __('Begin'); ?> - <?php echo - ($GLOBALS['cfg']['MainPageIconic'] - ? '<img src="' . $GLOBALS['pmaThemeImage'] . - 's_asc.png" width="11" height="9" align="middle" alt="" />' - : ''); ?> - </a> -<?php -if (! empty($section['title'])) { - echo $section['title']; + /** + * Displays the page + */ + ?> + <table id="tableprocesslist" class="data clearfloat"> + <thead> + <tr> + <th><?php echo __('Processes'); ?></th> + <th><?php echo __('ID'); ?></th> + <th><?php echo __('User'); ?></th> + <th><?php echo __('Host'); ?></th> + <th><?php echo __('Database'); ?></th> + <th><?php echo __('Command'); ?></th> + <th><?php echo __('Time'); ?></th> + <th><?php echo __('Status'); ?></th> + <th><?php + echo __('SQL query'); + if (!PMA_DRIZZLE) { ?> + <a href="<?php echo $full_text_link; ?>" + title="<?php echo empty($full) ? __('Show Full Queries') : __('Truncate Shown Queries'); ?>"> + <img src="<?php echo $GLOBALS['pmaThemeImage'] . 's_' . (empty($_REQUEST['full']) ? 'full' : 'partial'); ?>text.png" + alt="<?php echo empty($_REQUEST['full']) ? __('Show Full Queries') : __('Truncate Shown Queries'); ?>" /> + </a> + <? } ?> + </th> + </tr> + </thead> + <tbody> + <?php + $odd_row = true; + while($process = PMA_DBI_fetch_assoc($result)) { + if (PMA_DRIZZLE) { + // Drizzle uses uppercase keys + foreach ($process as $k => $v) { + $k = $k !== 'DB' + ? $k = ucfirst(strtolower($k)) + : 'db'; + $process[$k] = $v; + } + } + $url_params['kill'] = $process['Id']; + $kill_process = 'server_status.php' . PMA_generate_common_url($url_params); + ?> + <tr class="noclick <?php echo $odd_row ? 'odd' : 'even'; ?>"> + <td><a href="<?php echo $kill_process ; ?>"><?php echo __('Kill'); ?></a></td> + <td class="value"><?php echo $process['Id']; ?></td> + <td><?php echo $process['User']; ?></td> + <td><?php echo $process['Host']; ?></td> + <td><?php echo ((! isset($process['db']) || ! strlen($process['db'])) ? '<i>' . __('None') . '</i>' : $process['db']); ?></td> + <td><?php echo $process['Command']; ?></td> + <td class="value"><?php echo $process['Time']; ?></td> + <td><?php echo (empty($process['State']) ? '---' : $process['State']); ?></td> + <td><?php echo (empty($process['Info']) ? '---' : PMA_SQP_formatHtml(PMA_SQP_parse($process['Info']))); ?></td> + </tr> + <?php + $odd_row = ! $odd_row; + } + ?> + </tbody> + </table> + <?php } + +function printVariablesTable() { + global $server_status, $server_variables, $allocationMap, $links; + /** + * Messages are built using the message name + */ + $strShowStatus = Array( + 'Aborted_connects' => __('The number of failed attempts to connect to the MySQL server.'), + 'Binlog_cache_disk_use' => __('The number of transactions that used the temporary binary log cache but that exceeded the value of binlog_cache_size and used a temporary file to store statements from the transaction.'), + 'Binlog_cache_use' => __('The number of transactions that used the temporary binary log cache.'), + 'Connections' => __('The number of connection attempts (successful or not) to the MySQL server.'), + 'Created_tmp_disk_tables' => __('The number of temporary tables on disk created automatically by the server while executing statements. If Created_tmp_disk_tables is big, you may want to increase the tmp_table_size value to cause temporary tables to be memory-based instead of disk-based.'), + 'Created_tmp_files' => __('How many temporary files mysqld has created.'), + 'Created_tmp_tables' => __('The number of in-memory temporary tables created automatically by the server while executing statements.'), + 'Delayed_errors' => __('The number of rows written with INSERT DELAYED for which some error occurred (probably duplicate key).'), + 'Delayed_insert_threads' => __('The number of INSERT DELAYED handler threads in use. Every different table on which one uses INSERT DELAYED gets its own thread.'), + 'Delayed_writes' => __('The number of INSERT DELAYED rows written.'), + 'Flush_commands' => __('The number of executed FLUSH statements.'), + 'Handler_commit' => __('The number of internal COMMIT statements.'), + 'Handler_delete' => __('The number of times a row was deleted from a table.'), + 'Handler_discover' => __('The MySQL server can ask the NDB Cluster storage engine if it knows about a table with a given name. This is called discovery. Handler_discover indicates the number of time tables have been discovered.'), + 'Handler_read_first' => __('The number of times the first entry was read from an index. If this is high, it suggests that the server is doing a lot of full index scans; for example, SELECT col1 FROM foo, assuming that col1 is indexed.'), + 'Handler_read_key' => __('The number of requests to read a row based on a key. If this is high, it is a good indication that your queries and tables are properly indexed.'), + 'Handler_read_next' => __('The number of requests to read the next row in key order. This is incremented if you are querying an index column with a range constraint or if you are doing an index scan.'), + 'Handler_read_prev' => __('The number of requests to read the previous row in key order. This read method is mainly used to optimize ORDER BY ... DESC.'), + 'Handler_read_rnd' => __('The number of requests to read a row based on a fixed position. This is high if you are doing a lot of queries that require sorting of the result. You probably have a lot of queries that require MySQL to scan whole tables or you have joins that don\'t use keys properly.'), + 'Handler_read_rnd_next' => __('The number of requests to read the next row in the data file. This is high if you are doing a lot of table scans. Generally this suggests that your tables are not properly indexed or that your queries are not written to take advantage of the indexes you have.'), + 'Handler_rollback' => __('The number of internal ROLLBACK statements.'), + 'Handler_update' => __('The number of requests to update a row in a table.'), + 'Handler_write' => __('The number of requests to insert a row in a table.'), + 'Innodb_buffer_pool_pages_data' => __('The number of pages containing data (dirty or clean).'), + 'Innodb_buffer_pool_pages_dirty' => __('The number of pages currently dirty.'), + 'Innodb_buffer_pool_pages_flushed' => __('The number of buffer pool pages that have been requested to be flushed.'), + 'Innodb_buffer_pool_pages_free' => __('The number of free pages.'), + 'Innodb_buffer_pool_pages_latched' => __('The number of latched pages in InnoDB buffer pool. These are pages currently being read or written or that can\'t be flushed or removed for some other reason.'), + 'Innodb_buffer_pool_pages_misc' => __('The number of pages busy because they have been allocated for administrative overhead such as row locks or the adaptive hash index. This value can also be calculated as Innodb_buffer_pool_pages_total - Innodb_buffer_pool_pages_free - Innodb_buffer_pool_pages_data.'), + 'Innodb_buffer_pool_pages_total' => __('Total size of buffer pool, in pages.'), + 'Innodb_buffer_pool_read_ahead_rnd' => __('The number of "random" read-aheads InnoDB initiated. This happens when a query is to scan a large portion of a table but in random order.'), + 'Innodb_buffer_pool_read_ahead_seq' => __('The number of sequential read-aheads InnoDB initiated. This happens when InnoDB does a sequential full table scan.'), + 'Innodb_buffer_pool_read_requests' => __('The number of logical read requests InnoDB has done.'), + 'Innodb_buffer_pool_reads' => __('The number of logical reads that InnoDB could not satisfy from buffer pool and had to do a single-page read.'), + 'Innodb_buffer_pool_wait_free' => __('Normally, writes to the InnoDB buffer pool happen in the background. However, if it\'s necessary to read or create a page and no clean pages are available, it\'s necessary to wait for pages to be flushed first. This counter counts instances of these waits. If the buffer pool size was set properly, this value should be small.'), + 'Innodb_buffer_pool_write_requests' => __('The number writes done to the InnoDB buffer pool.'), + 'Innodb_data_fsyncs' => __('The number of fsync() operations so far.'), + 'Innodb_data_pending_fsyncs' => __('The current number of pending fsync() operations.'), + 'Innodb_data_pending_reads' => __('The current number of pending reads.'), + 'Innodb_data_pending_writes' => __('The current number of pending writes.'), + 'Innodb_data_read' => __('The amount of data read so far, in bytes.'), + 'Innodb_data_reads' => __('The total number of data reads.'), + 'Innodb_data_writes' => __('The total number of data writes.'), + 'Innodb_data_written' => __('The amount of data written so far, in bytes.'), + 'Innodb_dblwr_pages_written' => __('The number of pages that have been written for doublewrite operations.'), + 'Innodb_dblwr_writes' => __('The number of doublewrite operations that have been performed.'), + 'Innodb_log_waits' => __('The number of waits we had because log buffer was too small and we had to wait for it to be flushed before continuing.'), + 'Innodb_log_write_requests' => __('The number of log write requests.'), + 'Innodb_log_writes' => __('The number of physical writes to the log file.'), + 'Innodb_os_log_fsyncs' => __('The number of fsync() writes done to the log file.'), + 'Innodb_os_log_pending_fsyncs' => __('The number of pending log file fsyncs.'), + 'Innodb_os_log_pending_writes' => __('Pending log file writes.'), + 'Innodb_os_log_written' => __('The number of bytes written to the log file.'), + 'Innodb_pages_created' => __('The number of pages created.'), + 'Innodb_page_size' => __('The compiled-in InnoDB page size (default 16KB). Many values are counted in pages; the page size allows them to be easily converted to bytes.'), + 'Innodb_pages_read' => __('The number of pages read.'), + 'Innodb_pages_written' => __('The number of pages written.'), + 'Innodb_row_lock_current_waits' => __('The number of row locks currently being waited for.'), + 'Innodb_row_lock_time_avg' => __('The average time to acquire a row lock, in milliseconds.'), + 'Innodb_row_lock_time' => __('The total time spent in acquiring row locks, in milliseconds.'), + 'Innodb_row_lock_time_max' => __('The maximum time to acquire a row lock, in milliseconds.'), + 'Innodb_row_lock_waits' => __('The number of times a row lock had to be waited for.'), + 'Innodb_rows_deleted' => __('The number of rows deleted from InnoDB tables.'), + 'Innodb_rows_inserted' => __('The number of rows inserted in InnoDB tables.'), + 'Innodb_rows_read' => __('The number of rows read from InnoDB tables.'), + 'Innodb_rows_updated' => __('The number of rows updated in InnoDB tables.'), + 'Key_blocks_not_flushed' => __('The number of key blocks in the key cache that have changed but haven\'t yet been flushed to disk. It used to be known as Not_flushed_key_blocks.'), + 'Key_blocks_unused' => __('The number of unused blocks in the key cache. You can use this value to determine how much of the key cache is in use.'), + 'Key_blocks_used' => __('The number of used blocks in the key cache. This value is a high-water mark that indicates the maximum number of blocks that have ever been in use at one time.'), + 'Key_read_requests' => __('The number of requests to read a key block from the cache.'), + 'Key_reads' => __('The number of physical reads of a key block from disk. If Key_reads is big, then your key_buffer_size value is probably too small. The cache miss rate can be calculated as Key_reads/Key_read_requests.'), + 'Key_write_requests' => __('The number of requests to write a key block to the cache.'), + 'Key_writes' => __('The number of physical writes of a key block to disk.'), + 'Last_query_cost' => __('The total cost of the last compiled query as computed by the query optimizer. Useful for comparing the cost of different query plans for the same query. The default value of 0 means that no query has been compiled yet.'), + 'Max_used_connections' => __('The maximum number of connections that have been in use simultaneously since the server started.'), + 'Not_flushed_delayed_rows' => __('The number of rows waiting to be written in INSERT DELAYED queues.'), + 'Opened_tables' => __('The number of tables that have been opened. If opened tables is big, your table cache value is probably too small.'), + 'Open_files' => __('The number of files that are open.'), + 'Open_streams' => __('The number of streams that are open (used mainly for logging).'), + 'Open_tables' => __('The number of tables that are open.'), + 'Qcache_free_blocks' => __('The number of free memory blocks in query cache. High numbers can indicate fragmentation issues, which may be solved by issuing a FLUSH QUERY CACHE statement.'), + 'Qcache_free_memory' => __('The amount of free memory for query cache.'), + 'Qcache_hits' => __('The number of cache hits.'), + 'Qcache_inserts' => __('The number of queries added to the cache.'), + 'Qcache_lowmem_prunes' => __('The number of queries that have been removed from the cache to free up memory for caching new queries. This information can help you tune the query cache size. The query cache uses a least recently used (LRU) strategy to decide which queries to remove from the cache.'), + 'Qcache_not_cached' => __('The number of non-cached queries (not cachable, or not cached due to the query_cache_type setting).'), + 'Qcache_queries_in_cache' => __('The number of queries registered in the cache.'), + 'Qcache_total_blocks' => __('The total number of blocks in the query cache.'), + 'Rpl_status' => __('The status of failsafe replication (not yet implemented).'), + 'Select_full_join' => __('The number of joins that do not use indexes. If this value is not 0, you should carefully check the indexes of your tables.'), + 'Select_full_range_join' => __('The number of joins that used a range search on a reference table.'), + 'Select_range_check' => __('The number of joins without keys that check for key usage after each row. (If this is not 0, you should carefully check the indexes of your tables.)'), + 'Select_range' => __('The number of joins that used ranges on the first table. (It\'s normally not critical even if this is big.)'), + 'Select_scan' => __('The number of joins that did a full scan of the first table.'), + 'Slave_open_temp_tables' => __('The number of temporary tables currently open by the slave SQL thread.'), + 'Slave_retried_transactions' => __('Total (since startup) number of times the replication slave SQL thread has retried transactions.'), + 'Slave_running' => __('This is ON if this server is a slave that is connected to a master.'), + 'Slow_launch_threads' => __('The number of threads that have taken more than slow_launch_time seconds to create.'), + 'Slow_queries' => __('The number of queries that have taken more than long_query_time seconds.'), + 'Sort_merge_passes' => __('The number of merge passes the sort algorithm has had to do. If this value is large, you should consider increasing the value of the sort_buffer_size system variable.'), + 'Sort_range' => __('The number of sorts that were done with ranges.'), + 'Sort_rows' => __('The number of sorted rows.'), + 'Sort_scan' => __('The number of sorts that were done by scanning the table.'), + 'Table_locks_immediate' => __('The number of times that a table lock was acquired immediately.'), + 'Table_locks_waited' => __('The number of times that a table lock could not be acquired immediately and a wait was needed. If this is high, and you have performance problems, you should first optimize your queries, and then either split your table or tables or use replication.'), + 'Threads_cached' => __('The number of threads in the thread cache. The cache hit rate can be calculated as Threads_created/Connections. If this value is red you should raise your thread_cache_size.'), + 'Threads_connected' => __('The number of currently open connections.'), + 'Threads_created' => __('The number of threads created to handle connections. If Threads_created is big, you may want to increase the thread_cache_size value. (Normally this doesn\'t give a notable performance improvement if you have a good thread implementation.)'), + 'Threads_running' => __('The number of threads that are not sleeping.') + ); + + /** + * define some alerts + */ + // name => max value before alert + $alerts = array( + // lower is better + // variable => max value + 'Aborted_clients' => 0, + 'Aborted_connects' => 0, + + 'Binlog_cache_disk_use' => 0, + + 'Created_tmp_disk_tables' => 0, + + 'Handler_read_rnd' => 0, + 'Handler_read_rnd_next' => 0, + + 'Innodb_buffer_pool_pages_dirty' => 0, + 'Innodb_buffer_pool_reads' => 0, + 'Innodb_buffer_pool_wait_free' => 0, + 'Innodb_log_waits' => 0, + 'Innodb_row_lock_time_avg' => 10, // ms + 'Innodb_row_lock_time_max' => 50, // ms + 'Innodb_row_lock_waits' => 0, + + 'Slow_queries' => 0, + 'Delayed_errors' => 0, + 'Select_full_join' => 0, + 'Select_range_check' => 0, + 'Sort_merge_passes' => 0, + 'Opened_tables' => 0, + 'Table_locks_waited' => 0, + 'Qcache_lowmem_prunes' => 0, + + 'Qcache_free_blocks' => $server_status['Qcache_total_blocks'] / 5, + 'Slow_launch_threads' => 0, + + // depends on Key_read_requests + // normaly lower then 1:0.01 + 'Key_reads' => (0.01 * $server_status['Key_read_requests']), + // depends on Key_write_requests + // normaly nearly 1:1 + 'Key_writes' => (0.9 * $server_status['Key_write_requests']), + + 'Key_buffer_fraction' => 0.5, + + // alert if more than 95% of thread cache is in use + 'Threads_cached' => 0.95 * $server_variables['thread_cache_size'] + + // higher is better + // variable => min value + //'Handler read key' => '> ', + ); + ?> - </caption> +<table class="data sortable" id="serverstatusvariables"> <col class="namecol" /> <col class="valuecol" /> <col class="descrcol" /> @@ -776,35 +1009,21 @@ if (! empty($section['title'])) { <th><?php echo __('Description'); ?></th> </tr> </thead> -<?php - if (! empty($links[$section_name])) { -?> - <tfoot> + <!--<tfoot> <tr class="tblFooters"> <th colspan="3" class="tblFooters"> -<?php - foreach ($links[$section_name] as $link_name => $link_url) { - if ('doc' == $link_name) { - echo PMA_showMySQLDocu($link_url, $link_url); - } else { - echo '<a href="' . $link_url . '">' . $link_name . '</a>' . "\n"; - } - } - unset($link_url, $link_name); -?> </th> </tr> - </tfoot> -<?php - } -?> + </tfoot>--> <tbody> -<?php - $odd_row = false; - foreach ($section['vars'] as $name => $value) { + <? + + $odd_row = false; + foreach ($server_status as $name => $value) { $odd_row = !$odd_row; + // $allocations ?> - <tr class="noclick <?php echo $odd_row ? 'odd' : 'even'; ?>"> + <tr class="noclick <?php echo $odd_row ? 'odd' : 'even'; echo isset($allocationMap[$name])?' s_'.$allocationMap[$name]:''; ?>"> <th class="name"><?php echo htmlspecialchars($name) . PMA_showMySQLDocu('server-status-variables', 'server-status-variables', false, 'statvar_' . $name); ?> </th> <td class="value"><?php @@ -820,7 +1039,7 @@ if (! empty($section['title'])) { } elseif (is_numeric($value) && $value == (int) $value && $value > 1000) { echo PMA_formatNumber($value, 3, 1); } elseif (is_numeric($value) && $value == (int) $value) { - echo PMA_formatNumber($value, 4, 0); + echo PMA_formatNumber($value, 3, 0); } elseif (is_numeric($value)) { echo PMA_formatNumber($value, 3, 1); } else { @@ -832,8 +1051,8 @@ if (! empty($section['title'])) { ?></td> <td class="descr"> <?php - if (isset($GLOBALS['strShowStatus' . $name . 'Descr'])) { - echo $GLOBALS['strShowStatus' . $name . 'Descr']; + if (isset($strShowStatus[$name ])) { + echo $strShowStatus[$name]; }
if (isset($links[$name])) { @@ -850,41 +1069,64 @@ if (! empty($section['title'])) { ?> </td> </tr> -<?php - } - unset($name, $value); -?> + <?php + } + ?> </tbody> </table> -<?php + <?php +} + +function createQueryChart($com_vars=FALSE) { + /** + * Chart generation + */ + require_once './libraries/chart.lib.php'; + + if(!$com_vars) + $com_vars = PMA_DBI_fetch_result("SHOW GLOBAL STATUS LIKE 'Com\_%'", 0, 1); + + // admin commands are not queries (e.g. they include COM_PING, which is excluded from $server_status['Questions']) + unset($com_vars['Com_admin_commands']); + + arsort($com_vars); + + $merge_minimum = array_sum($com_vars) * 0.005; + $merged_value = 0; + + // remove zero values from the end, as well as merge together every value that is below 0.5% + // variable empty for Drizzle + if ($com_vars) { + while (($last_element=end($com_vars)) <= $merge_minimum) { + array_pop($com_vars); + $merged_value += $last_element; + } + + $com_vars['Other'] = $merged_value; + return PMA_chart_status($com_vars); } + + return ''; } -unset($section_name, $section, $sections, $server_status, $odd_row, $alerts); -?> -</div> -<?php -/* if the server works as master or slave in replication process, display useful information */ -if ($server_master_status || $server_slave_status) -{ -?> - <hr class="clearfloat" />
- <h3><a name="replication"></a><?php echo __('Replication status'); ?></h3> -<?php +/** + * cleanup of some deprecated values + */ +function cleanDeprecated(&$server_status) { + $deprecated = array( + 'Com_prepare_sql' => 'Com_stmt_prepare', + 'Com_execute_sql' => 'Com_stmt_execute', + 'Com_dealloc_sql' => 'Com_stmt_close', + );
- foreach ($replication_types as $type) - { - if (${"server_{$type}_status"}) { - PMA_replication_print_status_table($type); + foreach ($deprecated as $old => $new) { + if (isset($server_status[$old]) + && isset($server_status[$new])) { + unset($server_status[$old]); } } - unset($types); } -?> - -</div>
-<?php /** * Sends the footer */ diff --git a/server_variables.php b/server_variables.php index b04a049..00c8a85 100644 --- a/server_variables.php +++ b/server_variables.php @@ -14,6 +14,8 @@ if (! defined('PMA_NO_VARIABLES_IMPORT')) { } require_once './libraries/common.inc.php';
+$GLOBALS['js_include'][] = 'server_variables.js'; + /** * Does the common work */ @@ -52,7 +54,14 @@ $serverVarsGlobal = PMA_DBI_fetch_result('SHOW GLOBAL VARIABLES;', 0, 1); * Displays the page */ ?> -<table class="data"> +<fieldset id="tableFilter" style="display:none;"> +<legend>Filters</legend> +<div class="formelement"> + <label for="filterText">Containing the word:</label> + <input name="filterText" type="text" id="filterText" style="vertical-align: baseline;" /> +</div> +</fieldset> +<table class="data filteredData"> <thead> <tr><th><?php echo __('Variable'); ?></th> <th> diff --git a/sql.php b/sql.php index 61a4668..cb21b20 100644 --- a/sql.php +++ b/sql.php @@ -15,7 +15,14 @@ require_once './libraries/check_user_privileges.lib.php'; require_once './libraries/bookmark.lib.php';
$GLOBALS['js_include'][] = 'jquery/jquery-ui-1.8.custom.js'; -$GLOBALS['js_include'][] = 'pMap.js'; + +if(isset($_SESSION['profiling'])) { + $GLOBALS['js_include'][] = 'highcharts/highcharts.js'; + /* Files required for chart exporting */ + $GLOBALS['js_include'][] = 'highcharts/exporting.js'; + $GLOBALS['js_include'][] = 'canvg/canvg.js'; + $GLOBALS['js_include'][] = 'canvg/rgbcolor.js'; +}
/** * Defines the url to return to in case of error in a sql statement @@ -890,9 +897,40 @@ else { }
if (isset($profiling_results)) { - PMA_profilingResults($profiling_results, true); - } + // pma_token/url_query needed for chart export +?> +<script type="text/javascript"> +pma_token = '<?php echo $_SESSION[' PMA_token ']; ?>'; +url_query = '<?php echo isset($url_query)?$url_query:PMA_generate_common_url($db);?>'; +$(document).ready(createProfilingChart); +</script> +<? + echo '<fieldset><legend>' . __('Profiling') . '</legend>' . "\n"; + echo '<div style="float: left;">'; + echo '<table>' . "\n"; + echo ' <tr>' . "\n"; + echo ' <th>' . __('Status') . '</th>' . "\n"; + echo ' <th>' . __('Time') . '</th>' . "\n"; + echo ' </tr>' . "\n"; + + $chart_json = Array(); + foreach($profiling_results as $one_result) { + echo ' <tr>' . "\n"; + echo '<td>' . ucwords($one_result['Status']) . '</td>' . "\n"; + echo '<td align="right">' . (PMA_formatNumber($one_result['Duration'],3,1)) . 's</td>' . "\n"; + $chart_json[ucwords($one_result['Status'])] = $one_result['Duration']; + }
+ echo '</table>' . "\n"; + echo '</div>'; + //require_once './libraries/chart.lib.php'; + echo '<div id="profilingchart" style="display:none;">'; + //PMA_chart_profiling($profiling_results); + echo json_encode($chart_json); + echo '</div>'; + echo '</fieldset>' . "\n"; + } + // Displays the results in a table if (empty($disp_mode)) { // see the "PMA_setDisplayMode()" function in diff --git a/tbl_chart.php b/tbl_chart.php index 5853c45..817a8ea 100644 --- a/tbl_chart.php +++ b/tbl_chart.php @@ -19,7 +19,12 @@ if (! defined('PMA_NO_VARIABLES_IMPORT')) { */ require_once './libraries/common.inc.php';
-$GLOBALS['js_include'][] = 'pMap.js'; +$GLOBALS['js_include'][] = 'tbl_chart.js'; +$GLOBALS['js_include'][] = 'highcharts/highcharts.js'; +/* Files required for chart exporting */ +$GLOBALS['js_include'][] = 'highcharts/exporting.js'; +$GLOBALS['js_include'][] = 'canvg/canvg.js'; +$GLOBALS['js_include'][] = 'canvg/rgbcolor.js';
/** * Runs common work @@ -43,11 +48,6 @@ if (strlen($GLOBALS['table'])) { }
/* - * Import chart functions - */ -require_once './libraries/chart.lib.php'; - -/* * Execute the query and return the result */ $data = array(); @@ -63,129 +63,79 @@ if (PMA_isValid($_REQUEST['chartSettings'], 'array')) { $chartSettings = $_REQUEST['chartSettings']; }
-// get the chart and settings after chart generation -$chart = PMA_chart_results($data, $chartSettings); - -if (!empty($chart)) { - $message = PMA_Message::success(__('Chart generated successfully.')); -} -else { - $message = PMA_Message::error(__('The result of this query can't be used for a chart. See [a@./Documentation.html#faq6_29@Documentation]FAQ 6.29[/a]')); -} - $url_params['db'] = $GLOBALS['db']; $url_params['reload'] = 1;
/** * Displays the page */ +// pma_token/url_query needed for chart export ?> +<script type="text/javascript"> +pma_token = '<?php echo $_SESSION[' PMA_token ']; ?>'; +url_query = '<?php echo $url_query;?>'; +</script> <!-- Display Chart options --> <div id="div_view_options"> <form method="post" action="tbl_chart.php"> <?php echo PMA_generate_common_hidden_inputs($url_params); ?> <fieldset> <legend><?php echo __('Display chart'); ?></legend> - - <div style="float: right"> - <?php echo $chart; ?> + <div style="float:left;"> + <input type="radio" name="chartType" value="bar"><?php echo __('Bar'); ?> + <input type="radio" name="chartType" value="column"><?php echo __('Column'); ?> + <input type="radio" name="chartType" value="line" checked><?php echo __('Line'); ?> + <input type="radio" name="chartType" value="spline"><?php echo __('Spline'); ?> + <input type="radio" name="chartType" value="pie"><?php echo __('Pie'); ?> + <span class="barStacked" style="display:none;"> + <input type="checkbox" name="barStacked" value="1"><?php echo __('Stacked'); ?> + </span> + <br> + <input type="text" name="chartTitle" value="<?php echo __('Chart title'); ?>"> + <?php $keys = array_keys($data[0]); + $yaxis=-1; + if(count($keys)>1) { + echo '<br>'; + echo __('X-Axis:'); ?> <select name="chartXAxis"> + <?php + + foreach($keys as $idx=>$key) { + if($yaxis==-1 && ($idx==count($data[0])-1 || preg_match("/(date|time)/i",$key))) { + echo '<option value="'.$idx.'" selected>'.$key.'</option>'; + $yaxis=$idx; + } else { + echo '<option value="'.$idx.'">'.$key.'</option>'; + } + } + + ?> + </select><br> + <?php echo __('Series:'); ?> + <select name="chartSeries"> + <option value="columns"><?php echo __('The remaining columns'); ?></option> + <?php + foreach($keys as $idx=>$key) { + echo '<option>'.$key.'</option>'; + } + ?> + </select> + <?php + } + ?> + + </div> + <div style="float:left; padding-left:40px;"> + <?php echo __('X-Axis label:'); ?> <input style="margin-top:0;" type="text" name="xaxis_label" value="<?php echo ($yaxis==-1)?__('X Values'):$keys[$yaxis]; ?>"><br> + <?php echo __('Y-Axis label:'); ?> <input type="text" name="yaxis_label" value="<?php echo __('Y Values'); ?>"> + </div> + <p style="clear:both;"> </p> + <div id="resizer" style="width:600px; height:400px;"> + <div id="inner-resizer"> + <div id="querychart" style="display:none;"> + <?php echo json_encode($data); ?> + </div> + </div> </div> - - <input type="hidden" name="sql_query" id="sql_query" value="<?php echo htmlspecialchars($sql_query); ?>" /> - - <table> - <tr><td><label for="width"><?php echo __("Width"); ?></label></td> - <td><input type="text" name="chartSettings[width]" id="width" value="<?php echo (isset($chartSettings['width']) ? htmlspecialchars($chartSettings['width']) : ''); ?>" /></td> - </tr> - - <tr><td><label for="height"><?php echo __("Height"); ?></label></td> - <td><input type="text" name="chartSettings[height]" id="height" value="<?php echo (isset($chartSettings['height']) ? htmlspecialchars($chartSettings['height']) : ''); ?>" /></td> - </tr> - - <tr><td><label for="titleText"><?php echo __("Title"); ?></label></td> - <td><input type="text" name="chartSettings[titleText]" id="titleText" value="<?php echo (isset($chartSettings['titleText']) ? htmlspecialchars($chartSettings['titleText']) : ''); ?>" /></td> - </tr> - - <?php if ($chartSettings['type'] != 'pie' && $chartSettings['type'] != 'radar') { ?> - <tr><td><label for="xLabel"><?php echo __("X Axis label"); ?></label></td> - <td><input type="text" name="chartSettings[xLabel]" id="xLabel" value="<?php echo (isset($chartSettings['xLabel']) ? htmlspecialchars($chartSettings['xLabel']) : ''); ?>" /></td> - </tr> - - <tr><td><label for="yLabel"><?php echo __("Y Axis label"); ?></label></td> - <td><input type="text" name="chartSettings[yLabel]" id="yLabel" value="<?php echo (isset($chartSettings['yLabel']) ? htmlspecialchars($chartSettings['yLabel']) : ''); ?>" /></td> - </tr> - <?php } ?> - - <tr><td><label for="areaMargins"><?php echo __("Area margins"); ?></label></td> - <td> - <input type="text" name="chartSettings[areaMargins][]" size="2" value="<?php echo (isset($chartSettings['areaMargins'][0]) ? htmlspecialchars($chartSettings['areaMargins'][0]) : ''); ?>" /> - <input type="text" name="chartSettings[areaMargins][]" size="2" value="<?php echo (isset($chartSettings['areaMargins'][1]) ? htmlspecialchars($chartSettings['areaMargins'][1]) : ''); ?>" /> - <input type="text" name="chartSettings[areaMargins][]" size="2" value="<?php echo (isset($chartSettings['areaMargins'][2]) ? htmlspecialchars($chartSettings['areaMargins'][2]) : ''); ?>" /> - <input type="text" name="chartSettings[areaMargins][]" size="2" value="<?php echo (isset($chartSettings['areaMargins'][3]) ? htmlspecialchars($chartSettings['areaMargins'][3]) : ''); ?>" /> - </td> - </tr> - - <?php if ($chartSettings['legend'] == true) { ?> - <tr><td><label for="legendMargins"><?php echo __("Legend margins"); ?></label></td> - <td> - <input type="text" name="chartSettings[legendMargins][]" size="2" value="<?php echo htmlspecialchars($chartSettings['legendMargins'][0]); ?>" /> - <input type="text" name="chartSettings[legendMargins][]" size="2" value="<?php echo htmlspecialchars($chartSettings['legendMargins'][1]); ?>" /> - <input type="text" name="chartSettings[legendMargins][]" size="2" value="<?php echo htmlspecialchars($chartSettings['legendMargins'][2]); ?>" /> - <input type="text" name="chartSettings[legendMargins][]" size="2" value="<?php echo htmlspecialchars($chartSettings['legendMargins'][3]); ?>" /> - </td> - </tr> - <?php } ?> - - <tr><td><label for="type"><?php echo __("Type"); ?></label></td> - <td> - <input type="radio" name="chartSettings[type]" value="bar" <?php echo ($chartSettings['type'] == 'bar' ? 'checked' : ''); ?>><?php echo __('Bar'); ?> - <input type="radio" name="chartSettings[type]" value="line" <?php echo ($chartSettings['type'] == 'line' ? 'checked' : ''); ?>><?php echo __('Line'); ?> - <input type="radio" name="chartSettings[type]" value="radar" <?php echo ($chartSettings['type'] == 'radar' ? 'checked' : ''); ?>><?php echo __('Radar'); ?> - <?php if ($chartSettings['multi'] == false) { ?> - <input type="radio" name="chartSettings[type]" value="pie" <?php echo ($chartSettings['type'] == 'pie' ? 'checked' : ''); ?>><?php echo __('Pie'); ?> - <?php } ?> - </td> - </tr> - - <?php if ($chartSettings['type'] == 'bar' && isset($chartSettings['multi']) && $chartSettings['multi'] == true) { ?> - <tr><td><label for="barType"><?php echo __("Bar type"); ?></label></td> - <td> - <input type="radio" name="chartSettings[barType]" value="stacked" <?php echo ($chartSettings['barType'] == 'stacked' ? 'checked' : ''); ?>><?php echo __('Stacked'); ?> - <input type="radio" name="chartSettings[barType]" value="multi" <?php echo ($chartSettings['barType'] == 'multi' ? 'checked' : ''); ?>><?php echo __('Multi'); ?> - </td> - </tr> - <?php } ?> - - <tr><td><label for="continuous"><?php echo __("Continuous image"); ?></label></td> - <td> - <input type="checkbox" name="chartSettings[continuous]" id="continuous" <?php echo ($chartSettings['continuous'] == 'on' ? 'checked="checked"' : ''); ?>> - <?php echo PMA_showHint(PMA_sanitize(__('For compatibility reasons the chart image is segmented by default, select this to draw the whole chart in one image.'))) ?> - </td> - </tr> - - <tr><td><label for="fontSize"><?php echo __("Font size"); ?></label></td> - <td><input type="text" name="chartSettings[fontSize]" id="fontSize" value="<?php echo (isset($chartSettings['fontSize']) ? htmlspecialchars($chartSettings['fontSize']) : ''); ?>" /></td> - </tr> - - <?php if ($chartSettings['type'] == 'radar') { ?> - <tr><td colspan="2"> - <p> - <?php echo __('When drawing a radar chart all values are normalized to a range [0..10].'); ?> - </p> - </td></tr> - <?php } ?> - - <tr><td colspan="2"> - <p> - <?php echo __('Note that not every result table can be put to the chart. See <a href="./Documentation.html#faq6_29" target="Documentation">FAQ 6.29</a>'); ?> - </p> - </td></tr> - - </table> - -</fieldset> -<fieldset class="tblFooters"> - <input type="submit" name="displayChart" value="<?php echo __('Redraw'); ?>" /> </fieldset> </form> </div> diff --git a/themes/original/css/theme_right.css.php b/themes/original/css/theme_right.css.php index 855d611..f2bbfb8 100644 --- a/themes/original/css/theme_right.css.php +++ b/themes/original/css/theme_right.css.php @@ -678,6 +678,7 @@ ul#topmenu ul { list-style-type: none; display: none; border: 1px #666 solid; + z-index: 2; }
ul#topmenu li:hover ul, ul#topmenu .submenuhover ul { @@ -951,17 +952,58 @@ div#tablestatistics table {
/* serverstatus */ + +img.sortableIcon { + width:16px; + height:16px; + float:right; + background-repeat:no-repeat; +} + +table#serverstatusqueriesdetails th img.sortableIcon, table#serverstatusvariables th img.sortableIcon { + background-image:url(<?php echo $_SESSION['PMA_Theme']->getImgPath(); ?>s_sortable.png); +} +table#serverstatusqueriesdetails th.headerSortUp img.sortableIcon, table#serverstatusvariables th.headerSortUp img.sortableIcon { + background-image:url(<?php echo $_SESSION['PMA_Theme']->getImgPath(); ?>s_asc.png); +} +table#serverstatusqueriesdetails th.headerSortDown img.sortableIcon, table#serverstatusvariables th.headerSortDown img.sortableIcon { + background-image:url(<?php echo $_SESSION['PMA_Theme']->getImgPath(); ?>s_desc.png); +} + +.statuslinks { + float: <?php echo $right; ?>; +} + +/* Also used for the variables page */ +fieldset#tableFilter { + margin-bottom:1em; +} + +div#serverStatusTabs { + margin-top:1em; +} + div#serverstatus table caption a.top { float: <?php echo $right; ?>; }
-div#serverstatus div#serverstatusqueriesdetails table, -div#serverstatus table#serverstatustraffic, -div#serverstatus table#serverstatusconnections { +div#serverstatusquerieschart { + float:<?php echo $right; ?>; +} + +div#serverstatus table#serverstatusqueriesdetails { + float: <?php echo $left; ?>; +} + +table#serverstatustraffic { + float: <?php echo $left; ?>; +} +table#serverstatusconnections { float: <?php echo $left; ?>; + margin-<?php echo $left; ?>: 30px; }
-#serverstatussection, + .clearfloat { clear: both; } @@ -1013,6 +1055,15 @@ div#querywindowcontainer fieldset { } /* END querywindow */
+/* profiling */ + +div#profilingchart { + width:550px; + height:370px; + float:left; +} + +/* END profiling */
/* querybox */
@@ -1423,12 +1474,12 @@ select#db_select, select#table_select { }
.export_sub_options li.subgroup { - display: inline-block; - margin-top: 0; + display: inline-block; + margin-top: 0; }
.export_sub_options li { - margin-bottom: 0; + margin-bottom: 0; }
#quick_or_custom, #output_quick_export { @@ -1604,7 +1655,7 @@ iframe.IE_hack { padding: 0; list-style: none; color: #9A0000; - font-size: small; + font-size: small; }
.config-form fieldset th { diff --git a/themes/original/img/cleardot.gif b/themes/original/img/cleardot.gif new file mode 100644 index 0000000..35d42e8 Binary files /dev/null and b/themes/original/img/cleardot.gif differ diff --git a/themes/original/jquery/jquery-ui-1.8.custom.css b/themes/original/jquery/jquery-ui-1.8.custom.css index 1f4e40b..b2ad14f 100644 --- a/themes/original/jquery/jquery-ui-1.8.custom.css +++ b/themes/original/jquery/jquery-ui-1.8.custom.css @@ -51,7 +51,7 @@ .ui-widget .ui-widget { font-size: 1em; } .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; } .ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; } -.ui-widget-content a { color: #222222; } +/*.ui-widget-content a { color: #222222; }*/ .ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; } .ui-widget-header a { color: #222222; }
diff --git a/themes/pmahomme/css/theme_right.css.php b/themes/pmahomme/css/theme_right.css.php index 17ab26c..7092358 100644 --- a/themes/pmahomme/css/theme_right.css.php +++ b/themes/pmahomme/css/theme_right.css.php @@ -88,7 +88,7 @@ a:hover { }
#initials_table { - background:#f3f3f3; + background:#f3f3f3; border:1px solid #aaa; margin-bottom:10px; -moz-border-radius:5px; @@ -152,7 +152,7 @@ form { }
input[type=text]{ - border-radius:2px; + border-radius:2px; -moz-border-radius:2px; -webkit-border-radius:2px;
@@ -169,7 +169,7 @@ input[type=text]{ }
input[type=password]{ - border-radius:2px; + border-radius:2px; -moz-border-radius:2px; -webkit-border-radius:2px;
@@ -177,7 +177,7 @@ input[type=password]{ -moz-box-shadow:0 1px 2px #ddd; -webkit-box-shadow:0 1px 2px #ddd;
- background:url(./themes/pmahomme/img/input_bg.gif); + background:url(./themes/pmahomme/img/input_bg.gif); border:1px solid #aaa; color:#555555; padding:4px; @@ -188,17 +188,17 @@ input[type=password]{ input[type=submit]{ font-weight:bold; margin-left:14px; - border: 1px solid #aaa; - padding: 3px 7px; - color: #111; - text-decoration: none; - background: #ddd; + border: 1px solid #aaa; + padding: 3px 7px; + color: #111; + text-decoration: none; + background: #ddd;
border-radius: 12px; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; + -webkit-border-radius: 12px; + -moz-border-radius: 12px;
- text-shadow: 0px 1px 0px #fff; + text-shadow: 0px 1px 0px #fff;
background-image: url(./themes/svg_gradient.php?from=ffffff&to=cccccc); background-size: 100% 100%; @@ -219,8 +219,8 @@ input[type=submit]:hover{ position: relative; }
input[type=submit]:active{ position: relative; - top: 1px; - left: 1px; + top: 1px; + left: 1px; } textarea { overflow: visible; @@ -237,7 +237,7 @@ fieldset { padding: 1.5em; background: #eee; text-shadow:0 1px 0 #fff; - -moz-box-shadow: 1px 1px 2px #fff inset; + -moz-box-shadow: 1px 1px 2px #fff inset; -webkit-box-shadow: 1px 1px 2px #fff inset; box-shadow: 1px 1px 2px #fff inset; } @@ -284,20 +284,22 @@ table{border-collapse:collapse;} th{border-right:1px solid #fff; text-align:left;}
-img, -input, -select, -button { +img, button { vertical-align: middle; }
+input[type="checkbox"],input[type="radio"] { + vertical-align: -11%; +} + + select{ -moz-border-radius:2px; -webkit-border-radius:2px; border-radius:2px;
-moz-box-shadow:0 1px 2px #ddd; - -webkit-box-shadow:0 1px 2px #ddd; + -webkit-box-shadow:0 1px 2px #ddd; box-shadow:0 1px 2px #ddd;
border:1px solid #aaa; @@ -483,7 +485,7 @@ img.lightbulb {
/* MySQL Parser */ .syntax { - font-family: Verdan, Arial, Tahoma; + font-family: Verdan, Arial, Tahoma; font-size: 110%; }
@@ -656,7 +658,7 @@ div.footnotes { }
.error { - border:1px solid maroon !important; + border:1px solid maroon !important; color: #000; background:pink; } @@ -861,6 +863,7 @@ ul#topmenu ul { list-style-type: none; display: none; border: 1px #ddd solid; + z-index: 2; }
ul#topmenu li:hover { @@ -932,7 +935,7 @@ ul#topmenu > li {
/* default tab styles */ ul#topmenu a, ul#topmenu span { - padding:10px; + padding:10px; }
ul#topmenu ul a { @@ -1155,30 +1158,67 @@ text-shadow:0 1px 0 #000000;
/* serverstatus */ + +img.sortableIcon { + width:16px; + height:16px; + float:right; + background-repeat:no-repeat; +} + +table#serverstatusqueriesdetails th.headerSortUp img.sortableIcon, table#serverstatusvariables th.headerSortUp img.sortableIcon { + background-image:url(<?php echo $_SESSION['PMA_Theme']->getImgPath(); ?>s_asc.png); +} +table#serverstatusqueriesdetails th.headerSortDown img.sortableIcon, table#serverstatusvariables th.headerSortDown img.sortableIcon { + background-image:url(<?php echo $_SESSION['PMA_Theme']->getImgPath(); ?>s_desc.png); +} + +.statuslinks { + float: <?php echo $right; ?>; +} + +/* Also used for the variables page */ +fieldset#tableFilter { + margin-bottom:1em; +} + +div#serverStatusTabs { + margin-top:1em; +} + div#serverstatus table caption a.top { float: <?php echo $right; ?>; }
-div#serverstatus div#serverstatusqueriesdetails table, -div#serverstatus table#serverstatustraffic, -div#serverstatus table#serverstatusconnections { +div#serverstatusquerieschart { + float:<?php echo $right; ?>; +} + +div#serverstatus table#serverstatusqueriesdetails { float: <?php echo $left; ?>; }
-#serverstatussection, .clearfloat { clear: both; } -div#serverstatussection table { +table#serverstatusvariables { width: 100%; margin-bottom: 1em; } -div#serverstatussection table .name { +table#serverstatusvariables .name { width: 18em; + white-space:nowrap; } -div#serverstatussection table .value { +table#serverstatusvariables .value { width: 6em; } +table#serverstatustraffic { + float: <?php echo $left; ?>; +} +table#serverstatusconnections { + float: <?php echo $left; ?>; + margin-<?php echo $left; ?>: 30px; +}
div#serverstatus table tbody td.descr a, div#serverstatus table .tblFooters a { @@ -1205,6 +1245,26 @@ div#querywindowcontainer fieldset { } /* END querywindow */
+/* profiling */ + +div#profilingchart { + width:550px; + height:370px; + float:left; +} + +/* END profiling */ + +/* table charting */ + +#resizer { + border: 1px solid silver; +} +#inner-resizer { /* make room for the resize handle */ + padding: 10px; +} + +/* END table charting */
/* querybox */
@@ -1238,39 +1298,42 @@ div#querywindowcontainer fieldset { #serverstatus p a{color:#fff;text-decoration:underline;} #serverstatus h3 { - margin:35px 0px;font-weight:normal;color:#999;font-size:1.7em; + margin: 15px 0; + font-weight:normal; + color:#999; + font-size:1.7em; } #sectionlinks{ - padding:16px; + padding:16px; background:#f3f3f3; border:1px solid #aaa; border-radius:5px; -webkit-border-radius:5px; -moz-border-radius:5px; - box-shadow:0px 1px 1px #fff inset; + box-shadow:0px 1px 1px #fff inset; -webkit-box-shadow:0px 1px 1px #fff inset; -moz-box-shadow:0px 1px 1px #fff inset; } -#sectionlinks a, #statuslinks a{ - font-size:0.88em; +#sectionlinks a, .statuslinks a{ + font-size:0.88em; font-weight:bold; text-shadow: 0px 1px 0px #fff; line-height:35px; - margin-left:7px; - border: 1px solid #aaa; - padding: 5px 10px; - color: #111; - text-decoration: none; - background: #ddd; + margin-left:7px; + border: 1px solid #aaa; + padding: 5px 10px; + color: #111; + text-decoration: none; + background: #ddd; white-space: nowrap; - border-radius: 20px; - -webkit-border-radius: 20px; - -moz-border-radius: 20px; - box-shadow: 1px 1px 2px rgba(0,0,0,.5); - /* + border-radius: 20px; + -webkit-border-radius: 20px; + -moz-border-radius: 20px; + box-shadow: 1px 1px 2px rgba(0,0,0,.5); + /* -webkit-box-shadow: 1px 1px 2px rgba(0,0,0,.5); - -moz-box-shadow: 1px 1px 2px rgba(0,0,0,.5); - text-shadow: #fff 0px 1px 0px; + -moz-box-shadow: 1px 1px 2px rgba(0,0,0,.5); + text-shadow: #fff 0px 1px 0px; */ background-image: url(./themes/svg_gradient.php?from=ffffff&to=cccccc); background-size: 100% 100%; @@ -1279,7 +1342,7 @@ div#querywindowcontainer fieldset { background: -o-linear-gradient(top, #ffffff, #cccccc); <?php echo PMA_ieFilter('#ffffff', '#cccccc'); ?> } -#sectionlinks a:hover, #statuslinks a:hover{ +#sectionlinks a:hover, .statuslinks a:hover{ background-image: url(./themes/svg_gradient.php?from=cccccc&to=dddddd); background-size: 100% 100%; background: -webkit-gradient(linear, left top, left bottom, from(#cccccc), to(#dddddd)); @@ -1312,9 +1375,9 @@ textarea#sqlquery { -moz-border-radius:4px; -webkit-border-radius:4px; border-raduis:4px - border:1px solid #aaa; - padding:5px; - font-family:inherit; + border:1px solid #aaa; + padding:5px; + font-family:inherit; } textarea#sql_query_edit{ height:7em; @@ -1553,7 +1616,7 @@ code.sql, div.sqlvalidate { background-color: #bbb; padding: 0.1em 0.3em; margin-top: 0; - color:#fff; + color:#fff; font-size:1.6em; font-weight:normal; text-shadow:0 1px 0 #777; @@ -1690,17 +1753,17 @@ input[type=text].invalid_value, .exportoptions #buttonGo, .importoptions #buttonGo { font-weight:bold; margin-left:14px; - border: 1px solid #aaa; - padding: 5px 12px; - color: #111; - text-decoration: none; - background: #ddd; + border: 1px solid #aaa; + padding: 5px 12px; + color: #111; + text-decoration: none; + background: #ddd;
border-radius: 12px; - -webkit-border-radius: 12px; - -moz-border-radius: 12px; + -webkit-border-radius: 12px; + -moz-border-radius: 12px;
- text-shadow: 0px 1px 0px #fff; + text-shadow: 0px 1px 0px #fff;
background-image: url(./themes/svg_gradient.php?from=ffffff&to=cccccc); background-size: 100% 100%; @@ -1750,12 +1813,12 @@ select#db_select, select#table_select { }
.export_sub_options li.subgroup { - display: inline-block; - margin-top: 0; + display: inline-block; + margin-top: 0; }
.export_sub_options li { - margin-bottom: 0; + margin-bottom: 0; }
#quick_or_custom, #output_quick_export { @@ -1943,7 +2006,7 @@ iframe.IE_hack { padding: 0; list-style: none; color: #9A0000; - font-size: small; + font-size: small; }
.config-form fieldset th { diff --git a/themes/pmahomme/img/cleardot.gif b/themes/pmahomme/img/cleardot.gif new file mode 100644 index 0000000..35d42e8 Binary files /dev/null and b/themes/pmahomme/img/cleardot.gif differ diff --git a/themes/pmahomme/img/s_sortable.png b/themes/pmahomme/img/s_sortable.png new file mode 100644 index 0000000..0a34142 Binary files /dev/null and b/themes/pmahomme/img/s_sortable.png differ diff --git a/themes/pmahomme/jquery/jquery-ui-1.8.custom.css b/themes/pmahomme/jquery/jquery-ui-1.8.custom.css index 9446f01..d6a599f 100644 --- a/themes/pmahomme/jquery/jquery-ui-1.8.custom.css +++ b/themes/pmahomme/jquery/jquery-ui-1.8.custom.css @@ -51,7 +51,7 @@ .ui-widget .ui-widget { font-size: 1em; } .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif; font-size: 1em; } .ui-widget-content { border: 1px solid #aaaaaa; background: #ffffff url(images/ui-bg_flat_75_ffffff_40x100.png) 50% 50% repeat-x; color: #222222; } -.ui-widget-content a { color: #222222; } +/*.ui-widget-content a { color: #222222; }*/ .ui-widget-header { border: 1px solid #aaaaaa; background: #cccccc url(images/ui-bg_highlight-soft_75_cccccc_1x100.png) 50% 50% repeat-x; color: #222222; font-weight: bold; } .ui-widget-header a { color: #222222; }
hooks/post-receive