1. /* ===========================================================
  2. * JFreeChart : a free chart library for the Java(tm) platform
  3. * ===========================================================
  4. *
  5. * (C) Copyright 2000-2005, by Object Refinery Limited and Contributors.
  6. *
  7. * Project Info: http://www.jfree.org/jfreechart/index.html
  8. *
  9. * This library is free software; you can redistribute it and/or modify it
  10. * under the terms of the GNU Lesser General Public License as published by
  11. * the Free Software Foundation; either version 2.1 of the License, or
  12. * (at your option) any later version.
  13. *
  14. * This library is distributed in the hope that it will be useful, but
  15. * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  16. * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
  17. * License for more details.
  18. *
  19. * You should have received a copy of the GNU Lesser General Public License
  20. * along with this library; if not, write to the Free Software Foundation,
  21. * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
  22. *
  23. * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
  24. * in the United States and other countries.]
  25. *
  26. * -----------------
  27. * CategoryAxis.java
  28. * -----------------
  29. * (C) Copyright 2000-2005, by Object Refinery Limited.
  30. *
  31. * Original Author: David Gilbert;
  32. * Contributor(s): -;
  33. *
  34. * $Id: CategoryAxis.java,v 1.10 2005/03/01 11:47:47 mungady Exp $
  35. *
  36. * Changes (from 21-Aug-2001)
  37. * --------------------------
  38. * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG);
  39. * 18-Sep-2001 : Updated header (DG);
  40. * 04-Dec-2001 : Changed constructors to protected, and tidied up default
  41. * values (DG);
  42. * 19-Apr-2002 : Updated import statements (DG);
  43. * 05-Sep-2002 : Updated constructor for changes in Axis class (DG);
  44. * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG);
  45. * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
  46. * 22-Jan-2002 : Removed monolithic constructor (DG);
  47. * 26-Mar-2003 : Implemented Serializable (DG);
  48. * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into
  49. * this class (DG);
  50. * 13-Aug-2003 : Implemented Cloneable (DG);
  51. * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
  52. * 05-Nov-2003 : Fixed serialization bug (DG);
  53. * 26-Nov-2003 : Added category label offset (DG);
  54. * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised
  55. * category label position attributes (DG);
  56. * 07-Jan-2004 : Added new implementation for linewrapping of category
  57. * labels (DG);
  58. * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG);
  59. * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG);
  60. * 16-Mar-2004 : Added support for tooltips on category labels (DG);
  61. * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D
  62. * because of JDK bug 4976448 which persists on JDK 1.3.1 (DG);
  63. * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG);
  64. * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG);
  65. * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
  66. * release (DG);
  67. * 21-Jan-2005 : Modified return type for RectangleAnchor.coordinates()
  68. * method (DG);
  69. *
  70. */
  71. package org.jfree.chart.axis;
  72. import java.awt.Graphics2D;
  73. import java.awt.Insets;
  74. import java.awt.Shape;
  75. import java.awt.geom.Point2D;
  76. import java.awt.geom.Rectangle2D;
  77. import java.io.IOException;
  78. import java.io.ObjectInputStream;
  79. import java.io.ObjectOutputStream;
  80. import java.io.Serializable;
  81. import java.util.HashMap;
  82. import java.util.Iterator;
  83. import java.util.List;
  84. import java.util.Map;
  85. import org.jfree.chart.entity.EntityCollection;
  86. import org.jfree.chart.entity.TickLabelEntity;
  87. import org.jfree.chart.event.AxisChangeEvent;
  88. import org.jfree.chart.plot.CategoryPlot;
  89. import org.jfree.chart.plot.Plot;
  90. import org.jfree.chart.plot.PlotRenderingInfo;
  91. import org.jfree.text.G2TextMeasurer;
  92. import org.jfree.text.TextBlock;
  93. import org.jfree.text.TextUtilities;
  94. import org.jfree.ui.RectangleAnchor;
  95. import org.jfree.ui.RectangleEdge;
  96. import org.jfree.ui.Size2D;
  97. import org.jfree.util.Log;
  98. import org.jfree.util.LogContext;
  99. import org.jfree.util.ObjectUtilities;
  100. import org.jfree.util.ShapeUtilities;
  101. /**
  102. * An axis that displays categories.
  103. */
  104. public class CategoryAxis extends Axis implements Cloneable, Serializable {
  105. /**
  106. * The default margin for the axis (used for both lower and upper margins).
  107. */
  108. public static final double DEFAULT_AXIS_MARGIN = 0.05;
  109. /**
  110. * The default margin between categories (a percentage of the overall axis
  111. * length).
  112. */
  113. public static final double DEFAULT_CATEGORY_MARGIN = 0.20;
  114. /** The amount of space reserved at the start of the axis. */
  115. private double lowerMargin;
  116. /** The amount of space reserved at the end of the axis. */
  117. private double upperMargin;
  118. /** The amount of space reserved between categories. */
  119. private double categoryMargin;
  120. /** The maximum number of lines for category labels. */
  121. private int maximumCategoryLabelLines;
  122. /**
  123. * A ratio that is multiplied by the width of one category to determine the
  124. * maximum label width.
  125. */
  126. private float maximumCategoryLabelWidthRatio;
  127. /** The category label offset. */
  128. private int categoryLabelPositionOffset;
  129. /**
  130. * A structure defining the category label positions for each axis
  131. * location.
  132. */
  133. private CategoryLabelPositions categoryLabelPositions;
  134. /** Storage for the category label tooltips (if any). */
  135. private Map categoryLabelToolTips;
  136. /** Access to logging facilities. */
  137. protected static final LogContext logger
  138. = Log.createContext(CategoryAxis.class);
  139. /**
  140. * Creates a new category axis with no label.
  141. */
  142. public CategoryAxis() {
  143. this(null);
  144. }
  145. /**
  146. * Constructs a category axis, using default values where necessary.
  147. *
  148. * @param label the axis label (<code>null</code> permitted).
  149. */
  150. public CategoryAxis(String label) {
  151. super(label);
  152. this.lowerMargin = DEFAULT_AXIS_MARGIN;
  153. this.upperMargin = DEFAULT_AXIS_MARGIN;
  154. this.categoryMargin = DEFAULT_CATEGORY_MARGIN;
  155. this.maximumCategoryLabelLines = 1;
  156. this.maximumCategoryLabelWidthRatio = 0.0f;
  157. setTickMarksVisible(false); // not supported by this axis type yet
  158. this.categoryLabelPositionOffset = 4;
  159. this.categoryLabelPositions = CategoryLabelPositions.STANDARD;
  160. this.categoryLabelToolTips = new HashMap();
  161. }
  162. /**
  163. * Returns the lower margin for the axis.
  164. *
  165. * @return The margin.
  166. */
  167. public double getLowerMargin() {
  168. return this.lowerMargin;
  169. }
  170. /**
  171. * Sets the lower margin for the axis and sends an {@link AxisChangeEvent}
  172. * to all registered listeners.
  173. *
  174. * @param margin the margin as a percentage of the axis length (for
  175. * example, 0.05 is five percent).
  176. */
  177. public void setLowerMargin(double margin) {
  178. this.lowerMargin = margin;
  179. notifyListeners(new AxisChangeEvent(this));
  180. }
  181. /**
  182. * Returns the upper margin for the axis.
  183. *
  184. * @return The margin.
  185. */
  186. public double getUpperMargin() {
  187. return this.upperMargin;
  188. }
  189. /**
  190. * Sets the upper margin for the axis and sends an {@link AxisChangeEvent}
  191. * to all registered listeners.
  192. *
  193. * @param margin the margin as a percentage of the axis length (for
  194. * example, 0.05 is five percent).
  195. */
  196. public void setUpperMargin(double margin) {
  197. this.upperMargin = margin;
  198. notifyListeners(new AxisChangeEvent(this));
  199. }
  200. /**
  201. * Returns the category margin.
  202. *
  203. * @return The margin.
  204. */
  205. public double getCategoryMargin() {
  206. return this.categoryMargin;
  207. }
  208. /**
  209. * Sets the category margin and sends an {@link AxisChangeEvent} to all
  210. * registered listeners. The overall category margin is distributed over
  211. * N-1 gaps, where N is the number of categories on the axis.
  212. *
  213. * @param margin the margin as a percentage of the axis length (for
  214. * example, 0.05 is five percent).
  215. */
  216. public void setCategoryMargin(double margin) {
  217. this.categoryMargin = margin;
  218. notifyListeners(new AxisChangeEvent(this));
  219. }
  220. /**
  221. * Returns the maximum number of lines to use for each category label.
  222. *
  223. * @return The maximum number of lines.
  224. */
  225. public int getMaximumCategoryLabelLines() {
  226. return this.maximumCategoryLabelLines;
  227. }
  228. /**
  229. * Sets the maximum number of lines to use for each category label and
  230. * sends an {@link AxisChangeEvent} to all registered listeners.
  231. *
  232. * @param lines the maximum number of lines.
  233. */
  234. public void setMaximumCategoryLabelLines(int lines) {
  235. this.maximumCategoryLabelLines = lines;
  236. notifyListeners(new AxisChangeEvent(this));
  237. }
  238. /**
  239. * Returns the category label width ratio.
  240. *
  241. * @return The ratio.
  242. */
  243. public float getMaximumCategoryLabelWidthRatio() {
  244. return this.maximumCategoryLabelWidthRatio;
  245. }
  246. /**
  247. * Sets the maximum category label width ratio and sends an
  248. * {@link AxisChangeEvent} to all registered listeners.
  249. *
  250. * @param ratio the ratio.
  251. */
  252. public void setMaximumCategoryLabelWidthRatio(float ratio) {
  253. this.maximumCategoryLabelWidthRatio = ratio;
  254. notifyListeners(new AxisChangeEvent(this));
  255. }
  256. /**
  257. * Returns the offset between the axis and the category labels (before
  258. * label positioning is taken into account).
  259. *
  260. * @return The offset (in Java2D units).
  261. */
  262. public int getCategoryLabelPositionOffset() {
  263. return this.categoryLabelPositionOffset;
  264. }
  265. /**
  266. * Sets the offset between the axis and the category labels (before label
  267. * positioning is taken into account).
  268. *
  269. * @param offset the offset (in Java2D units).
  270. */
  271. public void setCategoryLabelPositionOffset(int offset) {
  272. this.categoryLabelPositionOffset = offset;
  273. notifyListeners(new AxisChangeEvent(this));
  274. }
  275. /**
  276. * Returns the category label position specification (this contains label
  277. * positioning info for all four possible axis locations).
  278. *
  279. * @return The positions (never <code>null</code>).
  280. */
  281. public CategoryLabelPositions getCategoryLabelPositions() {
  282. return this.categoryLabelPositions;
  283. }
  284. /**
  285. * Sets the category label position specification for the axis and sends an
  286. * {@link AxisChangeEvent} to all registered listeners.
  287. *
  288. * @param positions the positions (<code>null</code> not permitted).
  289. */
  290. public void setCategoryLabelPositions(CategoryLabelPositions positions) {
  291. if (positions == null) {
  292. throw new IllegalArgumentException("Null 'positions' argument.");
  293. }
  294. this.categoryLabelPositions = positions;
  295. notifyListeners(new AxisChangeEvent(this));
  296. }
  297. /**
  298. * Adds a tooltip to the specified category and sends an
  299. * {@link AxisChangeEvent} to all registered listeners.
  300. *
  301. * @param category the category (<code>null<code> not permitted).
  302. * @param tooltip the tooltip text (<code>null</code> permitted).
  303. */
  304. public void addCategoryLabelToolTip(Comparable category, String tooltip) {
  305. if (category == null) {
  306. throw new IllegalArgumentException("Null 'category' argument.");
  307. }
  308. this.categoryLabelToolTips.put(category, tooltip);
  309. notifyListeners(new AxisChangeEvent(this));
  310. }
  311. /**
  312. * Removes the tooltip for the specified category and sends an
  313. * {@link AxisChangeEvent} to all registered listeners.
  314. *
  315. * @param category the category (<code>null<code> not permitted).
  316. */
  317. public void removeCategoryLabelToolTip(Comparable category) {
  318. if (category == null) {
  319. throw new IllegalArgumentException("Null 'category' argument.");
  320. }
  321. this.categoryLabelToolTips.remove(category);
  322. notifyListeners(new AxisChangeEvent(this));
  323. }
  324. /**
  325. * Clears the category label tooltips and sends an {@link AxisChangeEvent}
  326. * to all registered listeners.
  327. */
  328. public void clearCategoryLabelToolTips() {
  329. this.categoryLabelToolTips.clear();
  330. notifyListeners(new AxisChangeEvent(this));
  331. }
  332. /**
  333. * Returns the Java 2D coordinate for a category.
  334. *
  335. * @param anchor the anchor point.
  336. * @param category the category index.
  337. * @param categoryCount the category count.
  338. * @param area the data area.
  339. * @param edge the location of the axis.
  340. *
  341. * @return The coordinate.
  342. */
  343. public double getCategoryJava2DCoordinate(CategoryAnchor anchor,
  344. int category,
  345. int categoryCount,
  346. Rectangle2D area,
  347. RectangleEdge edge) {
  348. double result = 0.0;
  349. if (anchor == CategoryAnchor.START) {
  350. result = getCategoryStart(category, categoryCount, area, edge);
  351. }
  352. else if (anchor == CategoryAnchor.MIDDLE) {
  353. result = getCategoryMiddle(category, categoryCount, area, edge);
  354. }
  355. else if (anchor == CategoryAnchor.END) {
  356. result = getCategoryEnd(category, categoryCount, area, edge);
  357. }
  358. return result;
  359. }
  360. /**
  361. * Returns the starting coordinate for the specified category.
  362. *
  363. * @param category the category.
  364. * @param categoryCount the number of categories.
  365. * @param area the data area.
  366. * @param edge the axis location.
  367. *
  368. * @return The coordinate.
  369. */
  370. public double getCategoryStart(int category, int categoryCount,
  371. Rectangle2D area,
  372. RectangleEdge edge) {
  373. double result = 0.0;
  374. if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
  375. result = area.getX() + area.getWidth() * getLowerMargin();
  376. }
  377. else if ((edge == RectangleEdge.LEFT)
  378. || (edge == RectangleEdge.RIGHT)) {
  379. result = area.getMinY() + area.getHeight() * getLowerMargin();
  380. }
  381. double categorySize = calculateCategorySize(categoryCount, area, edge);
  382. double categoryGapWidth = calculateCategoryGapSize(
  383. categoryCount, area, edge
  384. );
  385. result = result + category * (categorySize + categoryGapWidth);
  386. return result;
  387. }
  388. /**
  389. * Returns the middle coordinate for the specified category.
  390. *
  391. * @param category the category.
  392. * @param categoryCount the number of categories.
  393. * @param area the data area.
  394. * @param edge the axis location.
  395. *
  396. * @return The coordinate.
  397. */
  398. public double getCategoryMiddle(int category, int categoryCount,
  399. Rectangle2D area, RectangleEdge edge) {
  400. return getCategoryStart(category, categoryCount, area, edge)
  401. + calculateCategorySize(categoryCount, area, edge) / 2;
  402. }
  403. /**
  404. * Returns the end coordinate for the specified category.
  405. *
  406. * @param category the category.
  407. * @param categoryCount the number of categories.
  408. * @param area the data area.
  409. * @param edge the axis location.
  410. *
  411. * @return The coordinate.
  412. */
  413. public double getCategoryEnd(int category, int categoryCount,
  414. Rectangle2D area, RectangleEdge edge) {
  415. return getCategoryStart(category, categoryCount, area, edge)
  416. + calculateCategorySize(categoryCount, area, edge);
  417. }
  418. /**
  419. * Calculates the size (width or height, depending on the location of the
  420. * axis) of a category.
  421. *
  422. * @param categoryCount the number of categories.
  423. * @param area the area within which the categories will be drawn.
  424. * @param edge the axis location.
  425. *
  426. * @return The category size.
  427. */
  428. protected double calculateCategorySize(int categoryCount, Rectangle2D area,
  429. RectangleEdge edge) {
  430. double result = 0.0;
  431. double available = 0.0;
  432. if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
  433. available = area.getWidth();
  434. }
  435. else if ((edge == RectangleEdge.LEFT) || (edge == RectangleEdge.RIGHT)) {
  436. available = area.getHeight();
  437. }
  438. if (categoryCount > 1) {
  439. result = available * (1 - getLowerMargin() - getUpperMargin()
  440. - getCategoryMargin());
  441. result = result / categoryCount;
  442. }
  443. else {
  444. result = available * (1 - getLowerMargin() - getUpperMargin());
  445. }
  446. return result;
  447. }
  448. /**
  449. * Calculates the size (width or height, depending on the location of the
  450. * axis) of a category gap.
  451. *
  452. * @param categoryCount the number of categories.
  453. * @param area the area within which the categories will be drawn.
  454. * @param edge the axis location.
  455. *
  456. * @return The category gap width.
  457. */
  458. protected double calculateCategoryGapSize(int categoryCount,
  459. Rectangle2D area,
  460. RectangleEdge edge) {
  461. double result = 0.0;
  462. double available = 0.0;
  463. if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) {
  464. available = area.getWidth();
  465. }
  466. else if ((edge == RectangleEdge.LEFT)
  467. || (edge == RectangleEdge.RIGHT)) {
  468. available = area.getHeight();
  469. }
  470. if (categoryCount > 1) {
  471. result = available * getCategoryMargin() / (categoryCount - 1);
  472. }
  473. return result;
  474. }
  475. /**
  476. * Estimates the space required for the axis, given a specific drawing area.
  477. *
  478. * @param g2 the graphics device (used to obtain font information).
  479. * @param plot the plot that the axis belongs to.
  480. * @param plotArea the area within which the axis should be drawn.
  481. * @param edge the axis location (top or bottom).
  482. * @param space the space already reserved.
  483. *
  484. * @return The space required to draw the axis.
  485. */
  486. public AxisSpace reserveSpace(Graphics2D g2, Plot plot,
  487. Rectangle2D plotArea,
  488. RectangleEdge edge, AxisSpace space) {
  489. // create a new space object if one wasn't supplied...
  490. if (space == null) {
  491. space = new AxisSpace();
  492. }
  493. // if the axis is not visible, no additional space is required...
  494. if (!isVisible()) {
  495. return space;
  496. }
  497. // calculate the max size of the tick labels (if visible)...
  498. double tickLabelHeight = 0.0;
  499. double tickLabelWidth = 0.0;
  500. if (isTickLabelsVisible()) {
  501. g2.setFont(getTickLabelFont());
  502. AxisState state = new AxisState();
  503. refreshTicks(g2, state, plotArea, plotArea, edge);
  504. if (edge == RectangleEdge.TOP) {
  505. tickLabelHeight = state.getMax();
  506. }
  507. else if (edge == RectangleEdge.BOTTOM) {
  508. tickLabelHeight = state.getMax();
  509. }
  510. else if (edge == RectangleEdge.LEFT) {
  511. tickLabelWidth = state.getMax();
  512. }
  513. else if (edge == RectangleEdge.RIGHT) {
  514. tickLabelWidth = state.getMax();
  515. }
  516. }
  517. // get the axis label size and update the space object...
  518. Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge);
  519. double labelHeight = 0.0;
  520. double labelWidth = 0.0;
  521. if (RectangleEdge.isTopOrBottom(edge)) {
  522. labelHeight = labelEnclosure.getHeight();
  523. space.add(
  524. labelHeight + tickLabelHeight
  525. + this.categoryLabelPositionOffset, edge
  526. );
  527. }
  528. else if (RectangleEdge.isLeftOrRight(edge)) {
  529. labelWidth = labelEnclosure.getWidth();
  530. space.add(
  531. labelWidth + tickLabelWidth + this.categoryLabelPositionOffset,
  532. edge
  533. );
  534. }
  535. return space;
  536. }
  537. /**
  538. * Configures the axis against the current plot.
  539. */
  540. public void configure() {
  541. // nothing required
  542. }
  543. /**
  544. * Draws the axis on a Java 2D graphics device (such as the screen or a
  545. * printer).
  546. *
  547. * @param g2 the graphics device (<code>null</code> not permitted).
  548. * @param cursor the cursor location.
  549. * @param plotArea the area within which the axis should be drawn
  550. * (<code>null</code> not permitted).
  551. * @param dataArea the area within which the plot is being drawn
  552. * (<code>null</code> not permitted).
  553. * @param edge the location of the axis (<code>null</code> not permitted).
  554. * @param plotState collects information about the plot
  555. * (<code>null</code> permitted).
  556. *
  557. * @return The axis state (never <code>null</code>).
  558. */
  559. public AxisState draw(Graphics2D g2,
  560. double cursor,
  561. Rectangle2D plotArea,
  562. Rectangle2D dataArea,
  563. RectangleEdge edge,
  564. PlotRenderingInfo plotState) {
  565. if (logger.isDebugEnabled()) {
  566. logger.debug("Entering draw() method, cursor = " + cursor);
  567. }
  568. // if the axis is not visible, don't draw it...
  569. if (!isVisible()) {
  570. return new AxisState(cursor);
  571. }
  572. if (isAxisLineVisible()) {
  573. drawAxisLine(g2, cursor, dataArea, edge);
  574. }
  575. // draw the category labels and axis label
  576. AxisState state = new AxisState(cursor);
  577. state = drawCategoryLabels(
  578. g2, plotArea, dataArea, edge, state, plotState
  579. );
  580. state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
  581. return state;
  582. }
  583. /**
  584. * Draws the category labels and returns the updated axis state.
  585. *
  586. * @param g2 the graphics device (<code>null</code> not permitted).
  587. * @param plotArea the plot area (<code>null</code> not permitted).
  588. * @param dataArea the area inside the axes (<code>null</code> not
  589. * permitted).
  590. * @param edge the axis location (<code>null</code> not permitted).
  591. * @param state the axis state (<code>null</code> not permitted).
  592. * @param plotState collects information about the plot (<code>null</code>
  593. * permitted).
  594. *
  595. * @return The updated axis state (never <code>null</code>).
  596. */
  597. protected AxisState drawCategoryLabels(Graphics2D g2,
  598. Rectangle2D plotArea,
  599. Rectangle2D dataArea,
  600. RectangleEdge edge,
  601. AxisState state,
  602. PlotRenderingInfo plotState) {
  603. if (logger.isDebugEnabled()) {
  604. logger.debug(
  605. "Entering drawCategoryLabels() method, cursor = "
  606. + state.getCursor()
  607. );
  608. }
  609. if (state == null) {
  610. throw new IllegalArgumentException("Null 'state' argument.");
  611. }
  612. if (isTickLabelsVisible()) {
  613. g2.setFont(getTickLabelFont());
  614. g2.setPaint(getTickLabelPaint());
  615. List ticks = refreshTicks(g2, state, plotArea, dataArea, edge);
  616. state.setTicks(ticks);
  617. int categoryIndex = 0;
  618. Iterator iterator = ticks.iterator();
  619. while (iterator.hasNext()) {
  620. CategoryTick tick = (CategoryTick) iterator.next();
  621. g2.setPaint(getTickLabelPaint());
  622. CategoryLabelPosition position
  623. = this.categoryLabelPositions.getLabelPosition(edge);
  624. double x0 = 0.0;
  625. double x1 = 0.0;
  626. double y0 = 0.0;
  627. double y1 = 0.0;
  628. if (edge == RectangleEdge.TOP) {
  629. x0 = getCategoryStart(
  630. categoryIndex, ticks.size(), dataArea, edge
  631. );
  632. x1 = getCategoryEnd(
  633. categoryIndex, ticks.size(), dataArea, edge
  634. );
  635. y1 = state.getCursor() - this.categoryLabelPositionOffset;
  636. y0 = y1 - state.getMax();
  637. }
  638. else if (edge == RectangleEdge.BOTTOM) {
  639. x0 = getCategoryStart(
  640. categoryIndex, ticks.size(), dataArea, edge
  641. );
  642. x1 = getCategoryEnd(
  643. categoryIndex, ticks.size(), dataArea, edge
  644. );
  645. y0 = state.getCursor() + this.categoryLabelPositionOffset;
  646. y1 = y0 + state.getMax();
  647. }
  648. else if (edge == RectangleEdge.LEFT) {
  649. y0 = getCategoryStart(
  650. categoryIndex, ticks.size(), dataArea, edge
  651. );
  652. y1 = getCategoryEnd(
  653. categoryIndex, ticks.size(), dataArea, edge
  654. );
  655. x1 = state.getCursor() - this.categoryLabelPositionOffset;
  656. x0 = x1 - state.getMax();
  657. }
  658. else if (edge == RectangleEdge.RIGHT) {
  659. y0 = getCategoryStart(
  660. categoryIndex, ticks.size(), dataArea, edge
  661. );
  662. y1 = getCategoryEnd(
  663. categoryIndex, ticks.size(), dataArea, edge
  664. );
  665. x0 = state.getCursor() + this.categoryLabelPositionOffset;
  666. x1 = x0 - state.getMax();
  667. }
  668. Rectangle2D area = new Rectangle2D.Double(
  669. x0, y0, (x1 - x0), (y1 - y0)
  670. );
  671. Point2D anchorPoint = RectangleAnchor.coordinates(
  672. area, position.getCategoryAnchor()
  673. );
  674. TextBlock block = tick.getLabel();
  675. block.draw(
  676. g2,
  677. (float) anchorPoint.getX(), (float) anchorPoint.getY(),
  678. position.getLabelAnchor(),
  679. (float) anchorPoint.getX(), (float) anchorPoint.getY(),
  680. position.getAngle()
  681. );
  682. Shape bounds = block.calculateBounds(
  683. g2, (float) anchorPoint.getX(), (float) anchorPoint.getY(),
  684. position.getLabelAnchor(),
  685. (float) anchorPoint.getX(), (float) anchorPoint.getY(),
  686. position.getAngle()
  687. );
  688. if (plotState != null) {
  689. EntityCollection entities
  690. = plotState.getOwner().getEntityCollection();
  691. if (entities != null) {
  692. String tooltip
  693. = (String) this.categoryLabelToolTips.get(
  694. tick.getCategory()
  695. );
  696. entities.add(
  697. new TickLabelEntity(bounds, tooltip, null)
  698. );
  699. }
  700. }
  701. categoryIndex++;
  702. }
  703. if (edge.equals(RectangleEdge.TOP)) {
  704. double h = state.getMax();
  705. state.cursorUp(h);
  706. }
  707. else if (edge.equals(RectangleEdge.BOTTOM)) {
  708. double h = state.getMax();
  709. state.cursorDown(h);
  710. }
  711. else if (edge == RectangleEdge.LEFT) {
  712. double w = state.getMax();
  713. state.cursorLeft(w);
  714. }
  715. else if (edge == RectangleEdge.RIGHT) {
  716. double w = state.getMax();
  717. state.cursorRight(w);
  718. }
  719. }
  720. return state;
  721. }
  722. /**
  723. * Creates a temporary list of ticks that can be used when drawing the axis.
  724. *
  725. * @param g2 the graphics device (used to get font measurements).
  726. * @param state the axis state.
  727. * @param plotArea the area where the plot and axes will be drawn.
  728. * @param dataArea the area inside the axes.
  729. * @param edge the location of the axis.
  730. *
  731. * @return A list of ticks.
  732. */
  733. public List refreshTicks(Graphics2D g2,
  734. AxisState state,
  735. Rectangle2D plotArea,
  736. Rectangle2D dataArea,
  737. RectangleEdge edge) {
  738. List ticks = new java.util.ArrayList();
  739. // sanity check for data area...
  740. if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) {
  741. return ticks;
  742. }
  743. CategoryPlot plot = (CategoryPlot) getPlot();
  744. List categories = plot.getCategories();
  745. double max = 0.0;
  746. if (categories != null) {
  747. CategoryLabelPosition position
  748. = this.categoryLabelPositions.getLabelPosition(edge);
  749. float r = this.maximumCategoryLabelWidthRatio;
  750. if (r <= 0.0) {
  751. r = position.getWidthRatio();
  752. }
  753. float l = 0.0f;
  754. if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) {
  755. l = (float) calculateCategorySize(
  756. categories.size(), plotArea, edge
  757. );
  758. }
  759. else {
  760. if (RectangleEdge.isLeftOrRight(edge)) {
  761. l = (float) plotArea.getWidth();
  762. }
  763. else {
  764. l = (float) plotArea.getHeight();
  765. }
  766. }
  767. int categoryIndex = 0;
  768. Iterator iterator = categories.iterator();
  769. while (iterator.hasNext()) {
  770. Comparable category = (Comparable) iterator.next();
  771. TextBlock label = createLabel(category, l * r, edge, g2);
  772. if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) {
  773. max = Math.max(
  774. max, calculateTextBlockHeight(label, position, g2)
  775. );
  776. }
  777. else if (edge == RectangleEdge.LEFT
  778. || edge == RectangleEdge.RIGHT) {
  779. max = Math.max(
  780. max, calculateTextBlockWidth(label, position, g2)
  781. );
  782. }
  783. Tick tick = new CategoryTick(
  784. category, label,
  785. position.getLabelAnchor(), position.getRotationAnchor(),
  786. position.getAngle()
  787. );
  788. ticks.add(tick);
  789. categoryIndex = categoryIndex + 1;
  790. }
  791. }
  792. state.setMax(max);
  793. return ticks;
  794. }
  795. /**
  796. * Creates a label.
  797. *
  798. * @param category the category.
  799. * @param width the available width.
  800. * @param edge the edge on which the axis appears.
  801. * @param g2 the graphics device.
  802. *
  803. * @return A label.
  804. */
  805. protected TextBlock createLabel(Comparable category, float width,
  806. RectangleEdge edge, Graphics2D g2) {
  807. TextBlock label = TextUtilities.createTextBlock(
  808. category.toString(), getTickLabelFont(), getTickLabelPaint(),
  809. width, this.maximumCategoryLabelLines, new G2TextMeasurer(g2)
  810. );
  811. return label;
  812. }
  813. /**
  814. * A utility method for determining the width of a text block.
  815. *
  816. * @param block the text block.
  817. * @param position the position.
  818. * @param g2 the graphics device.
  819. *
  820. * @return The width.
  821. */
  822. protected double calculateTextBlockWidth(TextBlock block,
  823. CategoryLabelPosition position,
  824. Graphics2D g2) {
  825. Insets insets = getTickLabelInsets();
  826. Size2D size = block.calculateDimensions(g2);
  827. Rectangle2D box = new Rectangle2D.Double(
  828. 0.0, 0.0, size.getWidth(), size.getHeight()
  829. );
  830. Shape rotatedBox = ShapeUtilities.rotateShape(
  831. box, position.getAngle(), 0.0f, 0.0f
  832. );
  833. double w = rotatedBox.getBounds2D().getWidth()
  834. + insets.top + insets.bottom;
  835. return w;
  836. }
  837. /**
  838. * A utility method for determining the height of a text block.
  839. *
  840. * @param block the text block.
  841. * @param position the label position.
  842. * @param g2 the graphics device.
  843. *
  844. * @return The height.
  845. */
  846. protected double calculateTextBlockHeight(TextBlock block,
  847. CategoryLabelPosition position,
  848. Graphics2D g2) {
  849. Insets insets = getTickLabelInsets();
  850. Size2D size = block.calculateDimensions(g2);
  851. Rectangle2D box = new Rectangle2D.Double(
  852. 0.0, 0.0, size.getWidth(), size.getHeight()
  853. );
  854. Shape rotatedBox = ShapeUtilities.rotateShape(
  855. box, position.getAngle(), 0.0f, 0.0f
  856. );
  857. double h = rotatedBox.getBounds2D().getHeight()
  858. + insets.top + insets.bottom;
  859. return h;
  860. }
  861. /**
  862. * Creates a clone of the axis.
  863. *
  864. * @return A clone.
  865. *
  866. * @throws CloneNotSupportedException if some component of the axis does
  867. * not support cloning.
  868. */
  869. public Object clone() throws CloneNotSupportedException {
  870. Object clone = super.clone();
  871. return clone;
  872. }
  873. /**
  874. * Tests this axis for equality with an arbitrary object.
  875. *
  876. * @param obj the object (<code>null</code> permitted).
  877. *
  878. * @return A boolean.
  879. */
  880. public boolean equals(Object obj) {
  881. if (obj == this) {
  882. return true;
  883. }
  884. if (!(obj instanceof CategoryAxis)) {
  885. return false;
  886. }
  887. if (!super.equals(obj)) {
  888. return false;
  889. }
  890. CategoryAxis that = (CategoryAxis) obj;
  891. if (that.lowerMargin != this.lowerMargin) {
  892. return false;
  893. }
  894. if (that.upperMargin != this.upperMargin) {
  895. return false;
  896. }
  897. if (that.categoryMargin != this.categoryMargin) {
  898. return false;
  899. }
  900. if (that.maximumCategoryLabelWidthRatio
  901. != this.maximumCategoryLabelWidthRatio) {
  902. return false;
  903. }
  904. if (that.categoryLabelPositionOffset
  905. != this.categoryLabelPositionOffset) {
  906. return false;
  907. }
  908. if (!ObjectUtilities.equal(
  909. that.categoryLabelPositions, this.categoryLabelPositions
  910. )) {
  911. return false;
  912. }
  913. if (!ObjectUtilities.equal(
  914. that.categoryLabelToolTips, this.categoryLabelToolTips
  915. )) {
  916. return false;
  917. }
  918. return true;
  919. }
  920. /**
  921. * Provides serialization support.
  922. *
  923. * @param stream the output stream.
  924. *
  925. * @throws IOException if there is an I/O error.
  926. */
  927. private void writeObject(ObjectOutputStream stream) throws IOException {
  928. stream.defaultWriteObject();
  929. }
  930. /**
  931. * Provides serialization support.
  932. *
  933. * @param stream the input stream.
  934. *
  935. * @throws IOException if there is an I/O error.
  936. * @throws ClassNotFoundException if there is a classpath problem.
  937. */
  938. private void readObject(ObjectInputStream stream)
  939. throws IOException, ClassNotFoundException {
  940. stream.defaultReadObject();
  941. }
  942. }