root/trunk/thirdparty/cl-pdf/chart.lisp

Revision 2636, 18.4 kB (checked in by hans, 10 months ago)

add cl-pdf for pixel->pdf converter

Line 
1 ;;; cl-pdf copyright 2002-2005 Marc Battyani see license.txt for the details
2 ;;; You can reach me at marc.battyani@fractalconcept.com or marc@battyani.net
3 ;;; The homepage of cl-pdf is here: http://www.fractalconcept.com/asp/html/cl-pdf.html
4
5 (in-package #:pdf)
6
7 ;;WARNING this part of cl-pdf is an alpha version. Use with care!
8
9 ;; Basic charts: histogram, pie and plot-xy
10 ;; (Thanks to Carlos Ungil <Carlos.Ungil@cern.ch> for plot-xy)
11
12
13 ;; the pre and post draw functions must have this signature:
14 ;; (defun pre-draw-chart (object dx dy x-scale y-scale x-min y-min x-max y-max) ...)
15
16 (defvar *default-chart-font* (get-font))
17
18 (defclass chart-item ()
19   ((x :accessor x :initform 0 :initarg :x)
20    (y :accessor y :initform 0 :initarg :y)
21    (width :accessor width :initform 0 :initarg :width)
22    (height :accessor height :initform 0 :initarg :height)
23    (background-color :accessor background-color :initform '(1 1 1) :initarg :background-color)
24    (title :accessor title :initform "" :initarg :title)
25    (title-font :accessor title-font :initform *default-chart-font* :initarg :title-font)
26    (title-font-size :accessor title-font-size :initform 12 :initarg :title-font-size)
27    (title-color :accessor title-color :initform '(0 0 0) :initarg :title-color)
28    (line-width :accessor line-width :initform 0.5 :initarg :line-width)
29    (line-color :accessor line-color :initform '(0 0 0) :initarg :line-color)
30    (pre-draw-chart-fn :accessor pre-draw-chart-fn :initform nil :initarg :pre-draw-chart-fn)
31    (post-draw-chart-fn :accessor post-draw-chart-fn :initform nil :initarg :post-draw-chart-fn)
32    ))
33
34 (defclass axis (chart-item)
35   ((label-font :accessor label-font :initform *default-chart-font* :initarg :label-font)
36    (label-font-size :accessor label-font-size :initform 10.0 :initarg :label-font-size)
37    (label-position :accessor label-position :initform :center :initarg :label-position)
38    (label-rotation :accessor label-rotation :initform 0 :initarg :label-rotation)
39    (label-color :accessor label-color :initform '(0 0 0) :initarg :label-color)
40    (tick-length :accessor tick-length :initform 6 :initarg :tick-length)
41    (tick-width :accessor tick-width :initform 0.25 :initarg :tick-width)
42    (ticks-positions :accessor ticks-positions)
43    (ticks-separation :accessor ticks-separation)
44    ))
45
46 (defclass value-axis (axis)
47   ((min-value :accessor min-value :initform 0 :initarg :min-value)
48    (max-value :accessor max-value :initform 100 :initarg :max-value)
49    (locked-values :accessor locked-values :initform nil :initarg :locked-values)
50    (subtick-length :accessor subtick-length :initform 2 :initarg :subtick-length)
51    (subtick-width :accessor subtick-width :initform 0.25 :initarg :subtick-width)
52    (integer-tick :accessor integer-tick :initform nil :initarg :integer-tick)
53    (nb-ticks :accessor nb-ticks :initform 10 :initarg :nb-ticks)
54    (tick-value :accessor tick-value)
55    (axis-scale :accessor axis-scale)
56    (axis-min :accessor axis-min)
57    (axis-max :accessor axis-max)
58    (nb-subticks :accessor nb-subticks)
59    (format-string :accessor format-string :initform nil :initarg :format-string)
60    ))
61
62 (defmethod initialize-instance :after ((axis value-axis) &rest init-options &key &allow-other-keys)
63   (declare (ignore init-options))
64   (compute-scale axis))
65
66 (defclass histo-axis (axis)
67   ((label-names :accessor label-names :initform nil :initarg :label-names)
68    ))
69
70 (defclass vertical-value-axis (value-axis)
71   ())
72
73 (defclass horizontal-value-axis (value-axis)
74   ())
75
76 (defclass vertical-histo-axis (histo-axis)
77   ())
78
79 (defclass horizontal-histo-axis (histo-axis)
80   ())
81
82 (defmethod axis-size (axis)
83   (width axis))
84
85 (defmethod axis-size ((axis vertical-value-axis))
86   (height axis))
87
88 (defmethod axis-size ((axis vertical-histo-axis))
89   (height axis))
90
91 (defmethod draw-object (obj)
92   (declare (ignore obj)))
93
94 (defun nice-number (n approx integer-p)
95   (let* ((n10 (expt 10 (floor (log n 10))))
96          (nf (/ n n10))
97          (value
98           (loop for (max val) in (if approx '((1.5 1)(3 2)(7 5)(15 10)) '((1 1)(2 2)(5 5)(15 10)))
99                 when (<= nf max) do (return (* val n10)))))
100     (if integer-p
101         (max 1 (round value))
102         value)))
103
104 (defun compute-scale (axis)
105   (let* ((min (min-value axis))
106          (max (max-value axis))
107          d nfrac)
108     (when (= min max)
109       (if (zerop min)
110         (setf max 1.0)
111         (if (plusp max)
112           (setf min 0.0 max (* max 1.01))
113           (setf min (* min 1.01) max 0.0))))
114     (setf d (nice-number (/ (nice-number (- max min) nil (integer-tick axis)) (nb-ticks axis))
115                          t (integer-tick axis)))
116     (setf nfrac (max (- (floor (log d 10))) 0))
117     (setf (tick-value axis) d
118           (axis-min axis) (* d (floor min d))
119           (axis-max axis) (* d (ceiling max d))
120           (nb-ticks axis) (1+ (ceiling (- (axis-max axis)(axis-min axis)) d))
121           (axis-scale axis) (/ (axis-size axis)(- (axis-max axis) (axis-min axis)))
122           (ticks-separation axis)(/ (axis-size axis) (1- (nb-ticks axis)))
123           (format-string axis)
124           (or (format-string axis)
125               (if (integer-tick axis)
126                   "~d"
127                   (format nil "~~,~df" nfrac)))
128           (ticks-positions axis) (make-array (nb-ticks axis)))
129     (loop for tick from 0 below (nb-ticks axis)
130           for pos from 0 by (ticks-separation axis) do
131           (setf (aref (ticks-positions axis) tick) pos))))
132
133 (defmethod draw-object ((axis horizontal-histo-axis))
134   (with-saved-state
135     (set-line-width (line-width axis))
136     (apply #'set-rgb-stroke (line-color axis))
137     (translate (x axis) (y axis))
138     (move-to 0 0)
139     (line-to (width axis) 0)
140     (stroke)
141     (set-line-width (tick-width axis))
142     (move-to 0 0)
143     (line-to 0 (- (tick-length axis)))
144     (stroke)
145     (apply #'set-rgb-fill (label-color axis))
146     (loop with nb = (length (label-names axis))
147           with d  = (/ (width axis) nb)
148           with l = (- (tick-length axis))
149           with font-size = (label-font-size axis)
150           with max-width = (- d (* 0.6 font-size))
151           with text-y = (* -1.25 font-size)
152           for name in (label-names axis)
153           for tx from d by d
154           for text-x from (* 0.5 d) by d do
155           (move-to tx 0)
156           (line-to tx l)
157           (stroke)
158           (draw-centered-text text-x text-y name (label-font axis) font-size max-width))))
159
160 (defmethod draw-object ((axis vertical-value-axis))
161   (with-saved-state
162     (set-line-width (line-width axis))
163     (apply #'set-rgb-stroke (line-color axis))
164     (translate (x axis) (y axis))
165     (move-to 0 0)
166     (line-to 0 (height axis))
167     (stroke)
168     (set-line-width (tick-width axis))
169     (move-to 0 0)
170     (line-to (- (tick-length axis)) 0)
171     (stroke)
172     (apply #'set-rgb-fill (label-color axis))
173     (loop with nb = (nb-ticks axis)
174           with d  = (ticks-separation axis)
175           with l = (- (tick-length axis))
176           with font-size = (label-font-size axis)
177           with text-x = (* l 1.25)
178           with format = (format-string axis)
179           repeat nb
180           for value from (axis-min axis) by (tick-value axis)
181           for y from 0 by d
182           for text-y from (* -0.35 font-size) by d do
183           (move-to 0 y)
184           (line-to l y)
185           (stroke)
186           (when (integer-tick axis) (setf value (round value)))
187           (draw-left-text text-x text-y (format nil format value) (label-font axis) font-size))))
188
189 (defclass legend (chart-item)
190   ((label-font :accessor label-font :initform *default-chart-font* :initarg :label-font)
191    (label-font-size :accessor label-font-size :initform 10.0 :initarg :label-font-size)
192    (label-color :accessor label-color :initform '(0 0 0) :initarg :label-color)
193    (labels&colors :accessor labels&colors :initform () :initarg :labels&colors)
194    ))
195
196 (defmethod draw-object ((obj legend))
197   (let* ((nb (length (labels&colors obj)))
198          (font-size (label-font-size obj))
199          (space (* 0.2 font-size))
200          (line-height (+ space font-size))
201          (height (+ (* nb line-height) space))
202          (width (+ (* 3 space) font-size
203                    (reduce 'max (labels&colors obj)
204                            :key #'(lambda (lc) (text-width (first lc) (label-font obj)font-size))))))
205     (with-saved-state
206       (set-line-width (line-width obj))
207       (apply #'set-rgb-stroke (line-color obj))
208       (apply #'set-rgb-fill (background-color obj))
209       (translate (x obj) (+ (y obj)(* 0.5 (- (height obj) height))))
210       (basic-rect 0 0 width height)
211       (fill-and-stroke)
212       (basic-rect 0 0 width height)
213       (clip-path)
214       (end-path-no-op)
215       (loop with dx = font-size
216             with dy = font-size
217             with text-x = (+ dx (* 2 space))
218             for (name color) in (labels&colors obj)
219             for y downfrom (- height line-height) by line-height do
220             (apply #'set-rgb-fill color)
221             (basic-rect space y dx dy)
222             (fill-and-stroke)
223             (apply #'set-rgb-fill (label-color obj))
224             (in-text-mode
225              (move-text text-x y)
226              (set-font (label-font obj) font-size)
227              (show-text name))
228             ))))
229
230 (defclass histogram (chart-item)
231   ((label-names :accessor label-names :initform nil :initarg :label-names)
232    (series :accessor series :initform () :initarg :series)
233    (stacked-series :accessor stacked-series :initform () :initarg :stacked-series)
234    (labels&colors :accessor labels&colors :initform () :initarg :labels&colors)
235    (h-lines-width :accessor h-lines-width :initform 0.2 :initarg :h-lines-width)
236    (h-lines-color :accessor h-lines-color :initform '(0.5 0.5 0.5) :initarg :h-lines-color)
237    (groups-spacing :accessor groups-spacing :initform 0.2 :initarg :groups-spacing)
238    (x-axis :accessor x-axis)
239    (y-axis :accessor y-axis)
240    (legend :accessor legend :initform nil)
241    ))
242
243 (defmethod initialize-instance :after ((histo histogram) &rest init-options &key
244                                        x-axis-options y-axis-options legend-options &allow-other-keys)
245   (declare (ignore init-options))
246   (setf (y-axis histo)
247         (apply #'make-instance 'vertical-value-axis
248                :x (x histo) :y (y histo) :height (height histo)
249                (append y-axis-options
250                        (list
251                         :min-value (if (stacked-series histo)
252                                        0.0
253                                        (reduce #'min (mapcar #'(lambda (values)
254                                                                  (reduce #'min values))
255                                                              (series histo))))
256                         :max-value (reduce #'max
257                                            (if (stacked-series histo)
258                                                (apply 'mapcar #'(lambda (&rest values)
259                                                                   (apply '+ values))
260                                                       (series histo))
261                                                (mapcar #'(lambda (values)
262                                                            (reduce #'max values))
263                                                        (series histo))))))))
264   (setf (x-axis histo)
265         (apply #'make-instance 'horizontal-histo-axis
266                :x (x histo) :y (y histo) :width (width histo)
267                :label-names (label-names histo)
268                x-axis-options))
269   (when (> (length (series histo)) 1)
270     (setf (legend histo)
271         (apply #'make-instance 'legend
272                :x (+ (x histo) (width histo) 10) :y (y histo) :width 60 :height (height histo)
273                :labels&colors (labels&colors histo)
274                legend-options))))
275
276 (defmethod draw-object ((obj histogram))
277   (let* ((nb-series (if (stacked-series obj) 1 (length (series obj))))
278          (nb-values (length (first (series obj))))
279          (width (width obj))
280          (group-width (/ width nb-values))
281          (spacing (* (groups-spacing obj) group-width))
282          (bar-width (/ (- group-width spacing) nb-series))
283          (min-value (axis-min (y-axis obj)))
284          (scale (axis-scale (y-axis obj))))
285     (with-saved-state
286         (translate (x obj)(y obj))
287       (set-line-width (line-width obj))
288       (apply #'set-rgb-stroke (line-color obj))
289       (apply #'set-rgb-fill (background-color obj))
290       (basic-rect 0 0 width (height obj))
291       (fill-and-stroke)
292       (set-line-width (h-lines-width obj))
293       (apply #'set-rgb-stroke (h-lines-color obj))
294       (loop for tick-y across (ticks-positions (y-axis obj)) do
295             (move-to 0 tick-y)
296             (line-to width tick-y)
297             (stroke))
298       (set-line-width (line-width obj))
299       (apply #'set-rgb-stroke (line-color obj))
300       (if (stacked-series obj)
301           (loop for values in (apply 'mapcar 'list (series obj))
302                 for gx from (* 0.5 spacing) by bar-width
303                 for bx from gx by group-width do
304                 (loop for y = 0.0 then (+ y dy)
305                       for value in values
306                       for (nil color) in (labels&colors obj)
307                       for dy = (* value scale) do
308                       (apply #'set-rgb-fill color)
309                       (basic-rect bx y bar-width dy)
310                       (fill-and-stroke)))
311           (loop for serie in (series obj)
312                 for gx from (* 0.5 spacing) by bar-width
313                 for (nil color) in (labels&colors obj) do
314                 (apply #'set-rgb-fill color)
315                 (loop for value in serie
316                       for dy = (* (- value min-value) scale)
317                       for bx from gx by group-width do
318                       (basic-rect bx 0 bar-width dy)
319                       (fill-and-stroke))))))
320   (draw-object (x-axis obj))
321   (draw-object (y-axis obj))
322   (draw-object (legend obj)))
323
324
325 (defclass pie-chart (chart-item)
326   ((serie  :accessor serie  :initform () :initarg :serie)
327    (labels&colors :accessor labels&colors :initform () :initarg :labels&colors)
328    (legend :accessor legend :initform nil)
329    ))
330
331 (defmethod initialize-instance :after ((obj pie-chart) &rest init-options &key no-legend
332                                        legend-options &allow-other-keys)
333   (declare (ignore init-options))
334   (unless no-legend
335     (setf (legend obj)
336           (apply #'make-instance 'legend
337                  :x (+ (x obj) (width obj) 10) :y (y obj) :width 60 :height (height obj)
338                  :labels&colors (labels&colors obj)
339                  legend-options))))
340
341 (defmethod draw-object ((obj pie-chart))
342   (let* ((sum (reduce #'+ (serie obj)))
343          (radius (* (width obj) 0.45))
344          angles)
345     (when (zerop sum) (setf sum 1))
346     (setf angles (mapcar #'(lambda (v) (/ (* 2 pi v) sum)) (serie obj)))
347     (with-saved-state
348       (translate (+ (x obj)(* 0.5 (width obj)))(+ (y obj)(* 0.5 (height obj))))
349       (set-line-width (line-width obj))
350       (apply #'set-rgb-stroke (line-color obj))
351       (apply #'set-rgb-fill (background-color obj))
352       (fill-and-stroke)
353       (loop for angle in angles
354             for (nil color) in (labels&colors obj)
355             for start = 0 then end
356             for end = (+ start angle) do
357             (apply #'set-rgb-fill color)
358             (pie 0 0 radius start angle)
359             (fill-and-stroke))))
360   (draw-object (legend obj)))
361
362
363 (defmethod draw-object ((axis horizontal-value-axis))
364   (with-saved-state
365     (set-line-width (line-width axis))
366     (apply #'set-rgb-stroke (line-color axis))
367     (translate (x axis) (y axis))
368     (move-to 0 0)
369     (line-to (width axis) 0)
370     (stroke)
371     (set-line-width (tick-width axis))
372     (move-to 0 0)
373     (line-to 0 (- (tick-length axis)))
374     (stroke)
375     (apply #'set-rgb-fill (label-color axis))
376     (loop with nb = (nb-ticks axis)
377           with d  = (ticks-separation axis)
378           with l = (- (tick-length axis))
379           with font-size = (label-font-size axis)
380           with text-y = (+ (* l 1.25) (* -1 font-size))
381           with format = (format-string axis)
382           repeat nb
383           for value from (axis-min axis) by (tick-value axis)
384           for x from 0 by d
385           for text-x from (* -0.35 font-size) by d do
386           (move-to x 0)
387           (line-to x l)
388           (stroke)
389           (draw-centered-text text-x text-y (format nil format value) (label-font axis) font-size d))))
390
391 (defclass plot-xy (chart-item)
392   ((series :accessor series :initform () :initarg :series)
393    (labels&colors :accessor labels&colors :initform () :initarg :labels&colors)
394    (h-lines-width :accessor h-lines-width :initform 0.2 :initarg :h-lines-width)
395    (h-lines-color :accessor h-lines-color :initform '(0.5 0.5 0.5) :initarg :h-lines-color)
396    (point-radius :accessor point-radius :initform 2 :initarg :point-radius)
397    (x-axis :accessor x-axis)
398    (y-axis :accessor y-axis)
399    (legend :accessor legend :initform nil)
400    ))
401
402 (defmethod initialize-instance :after ((plot plot-xy) &rest init-options &key
403                                        x-axis-options y-axis-options legend-options &allow-other-keys)
404   (declare (ignore init-options))
405   (setf (y-axis plot)
406         (apply #'make-instance 'vertical-value-axis
407                :x (x plot) :y (y plot) :height (height plot)
408                (append y-axis-options
409                        (list
410                         :min-value
411                         (reduce #'min
412                                 (mapcar #'(lambda (values)
413                                             (reduce #'min
414                                                     (remove nil (mapcar #'second values))))
415                                         (series plot)))
416                         :max-value
417                         (reduce #'max
418                                 (mapcar #'(lambda (values)
419                                             (reduce #'max
420                                                     (remove nil (mapcar #'second values))))
421                                         (series plot)))))))
422   (setf (x-axis plot)
423         (apply #'make-instance 'horizontal-value-axis
424                :x (x plot) :y (y plot) :width (width plot)
425                (append x-axis-options
426                        (list
427                         :min-value
428                         (reduce #'min
429                                 (mapcar #'(lambda (values)
430                                             (reduce #'min
431                                                     (remove nil (mapcar #'first values))))
432                                         (series plot)))
433                         :max-value
434                         (reduce #'max
435                                 (mapcar #'(lambda (values)
436                                             (reduce #'max
437                                                     (remove nil (mapcar #'first values))))
438                                         (series plot)))))))
439   (when (labels&colors plot)
440     (setf (legend plot)
441         (apply #'make-instance 'legend
442                :x (or (getf legend-options :x) (+ (x plot) (width plot) 10))
443                :y (or (getf legend-options :y) (y plot))
444                :width 60 :height (height plot)
445                :labels&colors (labels&colors plot)
446                legend-options))))
447
448
449 (defmethod draw-object ((obj plot-xy))
450   (let* ((width (width obj))
451          (height (height obj))
452          (min-value-y (axis-min (y-axis obj)))
453          (min-value-x (axis-min (x-axis obj)))
454          (max-value-y (axis-max (y-axis obj)))
455          (max-value-x (axis-max (x-axis obj)))
456          (scale-y (axis-scale (y-axis obj)))
457          (scale-x (axis-scale (x-axis obj))))
458     (with-saved-state
459         (translate (x obj)(y obj))
460       (set-line-width (line-width obj))
461       (apply #'set-rgb-stroke (line-color obj))
462       (apply #'set-rgb-fill (background-color obj))
463       (basic-rect 0 0 width height)
464       (fill-and-stroke)
465       (when (pre-draw-chart-fn obj)
466         (funcall (pre-draw-chart-fn obj) obj width height scale-x scale-y
467                  min-value-x min-value-y max-value-x max-value-y))
468       (set-line-width (h-lines-width obj))
469       (apply #'set-rgb-stroke (h-lines-color obj))
470       (loop for tick-y across (ticks-positions (y-axis obj)) do
471            (move-to 0 tick-y)
472            (line-to width tick-y)
473            (stroke))
474       (loop for tick-x across (ticks-positions (x-axis obj)) do
475            (move-to tick-x 0)
476            (line-to tick-x height)
477            (stroke))
478       (set-line-width (line-width obj))
479       (set-line-join 2)
480       (loop for serie in (series obj)
481             for (nil color) in (labels&colors obj) do
482            (apply #'set-rgb-stroke color)
483            (apply #'set-rgb-fill color)
484            (let ((points '())
485                  (all-points '()))
486              (loop for (xx yy) in serie
487                 for x = (when xx (* (- xx min-value-x) scale-x))
488                 for y = (when yy (* (- yy min-value-y) scale-y))
489                 do
490                   (if (and x y)
491                       (progn
492                         (push (list x y) points)
493                         (unless (zerop (point-radius obj))
494                           (circle x y (point-radius obj))
495                           (fill-and-stroke)))
496                       (when points
497                         (push points all-points)
498                         (setf points '()))))
499              (when points (push points all-points))
500              (map nil 'polyline all-points)
501              (stroke)))
502       (when (post-draw-chart-fn obj)
503         (funcall (post-draw-chart-fn obj) obj width height scale-x scale-y
504                  min-value-x min-value-y max-value-x max-value-y))))
505   (draw-object (x-axis obj))
506   (draw-object (y-axis obj))
507   (draw-object (legend obj)))
Note: See TracBrowser for help on using the browser.