|
| 1 | +/* Programming contest management system |
| 2 | + * Copyright © 2012 Luca Wehrstedt <luca.wehrstedt@gmail.com> |
| 3 | + * |
| 4 | + * This program is free software: you can redistribute it and/or modify |
| 5 | + * it under the terms of the GNU Affero General Public License as |
| 6 | + * published by the Free Software Foundation, either version 3 of the |
| 7 | + * License, or (at your option) any later version. |
| 8 | + * |
| 9 | + * This program is distributed in the hope that it will be useful, |
| 10 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | + * GNU Affero General Public License for more details. |
| 13 | + * |
| 14 | + * You should have received a copy of the GNU Affero General Public License |
| 15 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | + */ |
| 17 | + |
| 18 | +var Chart = new function () { |
| 19 | + var self = this; |
| 20 | + |
| 21 | + self.draw_chart = function (canvas, y_min, y_max, y_def, h_def, x_int, data, color, marks) { |
| 22 | + // canvas is the context |
| 23 | +/* |
| 24 | + canvas (GWTCanvas): the canvas this chart will be drawn on |
| 25 | + y_min (float): the y value corresponding to the bottom of the chart |
| 26 | + y_max (float): the y value corresponding to the top of the chart |
| 27 | + (note: y_min can be grater than y_max - the chart will be upside-down) |
| 28 | + y_def (float): the default y value (the line will start at that value) |
| 29 | + h_def (float): the default height of the colored area |
| 30 | + x_int (list of tuples of float): the list of x intervals to be drawn, |
| 31 | + in the form [begin, end) |
| 32 | + data (list of tuples of float): the data to be drawn, in the form (x, y, h) |
| 33 | + color (tuple of int): the r, g and b components of the color for the line |
| 34 | + marks (list of float): the y values at which horizontal lines will be drawn |
| 35 | +*/ |
| 36 | + |
| 37 | + // width and height |
| 38 | + var wid = canvas.width; |
| 39 | + var hei = canvas.height; |
| 40 | + |
| 41 | + // the padding around the chart |
| 42 | + var pad_l = 22; |
| 43 | + var pad_r = 1; |
| 44 | + var pad_t = 6; |
| 45 | + var pad_b = 6; |
| 46 | + |
| 47 | + // the intervals of allowed x values |
| 48 | + var x_size = 0; |
| 49 | + for (var i in x_int) { |
| 50 | + x_size += x_int[i][1] - x_int[i][0]; |
| 51 | + } |
| 52 | + |
| 53 | + // convert values to canvas coordinates |
| 54 | + var get_x = function (x) { |
| 55 | + return pad_l + x * (wid - pad_l - pad_r) / x_size; |
| 56 | + }; |
| 57 | + |
| 58 | + var get_y = function (y) { |
| 59 | + return pad_t + (y_max - y) * (hei - pad_t - pad_b) / (y_max - y_min == 0? 1: y_max - y_min); |
| 60 | + }; |
| 61 | + |
| 62 | + // clear the canvas |
| 63 | + canvas.width = wid; |
| 64 | + |
| 65 | + // get the context |
| 66 | + var context = canvas.getContext("2d"); |
| 67 | + |
| 68 | + // draw the axes |
| 69 | + context.lineWidth = 2; |
| 70 | + context.strokeStyle = "#dddddd"; |
| 71 | + |
| 72 | + context.beginPath(); |
| 73 | + context.moveTo(pad_l, pad_t); |
| 74 | + context.lineTo(pad_l, hei - pad_b); |
| 75 | + context.lineTo(wid - pad_r, hei - pad_b); |
| 76 | + context.lineTo(wid - pad_r, pad_t); |
| 77 | + context.stroke(); |
| 78 | + |
| 79 | + // draw horizontal markers |
| 80 | + context.lineWidth = 1; |
| 81 | + context.moveTo(pad_l, pad_t); |
| 82 | + context.lineTo(wid - pad_r, pad_t); |
| 83 | + context.stroke(); |
| 84 | + for (var i in marks) { |
| 85 | + context.beginPath(); |
| 86 | + context.moveTo(get_x(0), get_y(marks[i])); |
| 87 | + context.lineTo(get_x(x_size), get_y(marks[i])); |
| 88 | + context.stroke(); |
| 89 | + } |
| 90 | + |
| 91 | + // draw labels on the axes |
| 92 | + context.fillStyle = "#000000"; |
| 93 | + context.textAlign = "right"; |
| 94 | + context.textBaseline = "middle"; |
| 95 | + if (y_min != y_max) |
| 96 | + context.fillText(y_min.toString(), 18, hei - pad_b); |
| 97 | + context.fillText(y_max.toString(), 18, pad_t); |
| 98 | + for (var i in marks) { |
| 99 | + context.fillText(marks[i].toString(), 18, get_y(marks[i])); |
| 100 | + } |
| 101 | + |
| 102 | + var i = 0 // index of current interval |
| 103 | + var x_cum = 0 // cumulated x value (sum of the size of the first i-1 intervals) |
| 104 | + var x_pos = 0 // current x value |
| 105 | + var y_pos = y_def // current y value |
| 106 | + var h_pos = h_def // current h value |
| 107 | + var x_b = 0 // the 'begin' value of the current interval |
| 108 | + var x_e = 0 // the 'end' value of the current interval |
| 109 | + |
| 110 | + var tops = [[x_pos, y_pos]] // points of the line marking the top of the area |
| 111 | + var bots = [[x_pos, y_pos + h_pos]] // points of the line marking the bottom |
| 112 | + |
| 113 | + // helper method to open an interval |
| 114 | + var open_group = function () { |
| 115 | + context.lineWidth = 2; |
| 116 | + context.strokeStyle = "rgb(" + color[0] + "," + color[1] + "," + color[2] + ")"; |
| 117 | + context.beginPath(); |
| 118 | + x_b = x_int[i][0]; |
| 119 | + x_e = x_int[i][1]; |
| 120 | + context.moveTo(get_x(x_pos), get_y(y_pos)); |
| 121 | + } |
| 122 | + |
| 123 | + // helper method to close an interval |
| 124 | + var close_group = function () { |
| 125 | + x_cum += x_e - x_b; |
| 126 | + x_pos = x_cum; |
| 127 | + context.lineTo(get_x(x_pos), get_y(y_pos)); |
| 128 | + tops.push([x_pos, y_pos]); |
| 129 | + bots.push([x_pos, y_pos + h_pos]) |
| 130 | + context.stroke(); |
| 131 | + } |
| 132 | + |
| 133 | + // helper method to draw a separator |
| 134 | + var draw_separator = function () { |
| 135 | + context.lineWidth = 2; |
| 136 | + context.strokeStyle = "#dddddd"; |
| 137 | + context.beginPath(); |
| 138 | + context.moveTo(get_x(x_pos), get_y(y_min)); |
| 139 | + context.lineTo(get_x(x_pos), get_y(y_max)); |
| 140 | + context.stroke(); |
| 141 | + } |
| 142 | + |
| 143 | + open_group(); |
| 144 | + |
| 145 | + for (var idx in data) { |
| 146 | + var x = data[idx][0]; |
| 147 | + var y = data[idx][1]; |
| 148 | + var h = data[idx][2]; |
| 149 | + |
| 150 | + while (i < x_int.length && x_e <= x) { |
| 151 | + close_group(); |
| 152 | + i += 1; |
| 153 | + if (i < x_int.length) { |
| 154 | + draw_separator(); |
| 155 | + open_group(); |
| 156 | + } else { |
| 157 | + x_b = 0; |
| 158 | + x_e = 0; |
| 159 | + } |
| 160 | + } |
| 161 | + if (x_b <= x && x < x_e) { |
| 162 | + x_pos = x_cum + x - x_b; |
| 163 | + context.lineTo(get_x(x_pos), get_y(y_pos)); |
| 164 | + tops.push([x_pos, y_pos]); |
| 165 | + bots.push([x_pos, y_pos + h_pos]); |
| 166 | + y_pos = y; |
| 167 | + h_pos = h; |
| 168 | + context.lineTo(get_x(x_pos), get_y(y_pos)); |
| 169 | + tops.push([x_pos, y_pos]); |
| 170 | + bots.push([x_pos, y_pos + h_pos]); |
| 171 | + } else { |
| 172 | + y_pos = y; |
| 173 | + h_pos = h; |
| 174 | + context.moveTo(get_x(x_pos), get_y(y_pos)); |
| 175 | + tops.push([x_pos, y_pos]); |
| 176 | + bots.push([x_pos, y_pos + h_pos]); |
| 177 | + } |
| 178 | + } |
| 179 | + if (i < x_int.length) { |
| 180 | + close_group(); |
| 181 | + i += 1; |
| 182 | + } |
| 183 | + while (i < x_int.length) { |
| 184 | + draw_separator(); |
| 185 | + open_group(); |
| 186 | + close_group(); |
| 187 | + i += 1; |
| 188 | + } |
| 189 | + |
| 190 | + context.fillStyle = "rgba(" + color[0] + "," + color[1] + "," + color[2] + ",0.3)"; |
| 191 | + context.beginPath(); |
| 192 | + context.moveTo(get_x(tops[0][0]), get_y(tops[0][1])); |
| 193 | + for (var i = 0; i < tops.length; i += 1) { |
| 194 | + context.lineTo(get_x(tops[i][0]), get_y(tops[i][1])); |
| 195 | + } |
| 196 | + for (var i = bots.length - 1; i >= 0; i -= 1) { |
| 197 | + context.lineTo(get_x(bots[i][0]), get_y(bots[i][1])); |
| 198 | + } |
| 199 | + context.closePath(); |
| 200 | + context.fill(); |
| 201 | + }; |
| 202 | +}; |
0 commit comments