org.pepstock.charba.client.impl.plugins.HtmlLegend Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of charba Show documentation
Show all versions of charba Show documentation
Charba - J2CL and GWT charts library based on CHART.JS
/**
Copyright 2017 Andrea "Stock" Stocchero
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.pepstock.charba.client.impl.plugins;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.pepstock.charba.client.IsChart;
import org.pepstock.charba.client.colors.tiles.TilesFactory;
import org.pepstock.charba.client.configuration.Legend;
import org.pepstock.charba.client.defaults.IsDefaultScaledOptions;
import org.pepstock.charba.client.dom.BaseElement;
import org.pepstock.charba.client.dom.BaseHtmlElement;
import org.pepstock.charba.client.dom.BaseNode;
import org.pepstock.charba.client.dom.DOMBuilder;
import org.pepstock.charba.client.dom.NodeList;
import org.pepstock.charba.client.dom.elements.Canvas;
import org.pepstock.charba.client.dom.elements.Div;
import org.pepstock.charba.client.dom.elements.TableCell;
import org.pepstock.charba.client.dom.enums.Unit;
import org.pepstock.charba.client.dom.safehtml.SafeHtml;
import org.pepstock.charba.client.enums.DefaultPlugin;
import org.pepstock.charba.client.enums.Position;
import org.pepstock.charba.client.impl.plugins.HtmlLegendOptionsFactory.HtmlLegendBuilderDefaultsOptionsFactory;
import org.pepstock.charba.client.items.LegendLabelItem;
import org.pepstock.charba.client.plugins.AbstractPlugin;
/**
* This plugin implements a HTML legend in order to give more flexibility to who needs to customize the legend.
* It uses the {@link HtmlLegendLabelsCallback} to generated HTML legend.
*
* @author Andrea "Stock" Stocchero
*/
public final class HtmlLegend extends AbstractPlugin {
/**
* Plugin ID {@value ID}.
*/
public static final String ID = "charbahtmllegend";
/**
* The factory to create options for plugin.
*/
public static final HtmlLegendOptionsFactory FACTORY = new HtmlLegendOptionsFactory();
// factory instance to read the options from default global
static final HtmlLegendBuilderDefaultsOptionsFactory DEFAULTS_FACTORY = new HtmlLegendBuilderDefaultsOptionsFactory();
// singleton instance
private static final HtmlLegend INSTANCE = new HtmlLegend();
// suffix label for main HTML legend element id
private static final String SUFFIX_LEGEND_ELEMENT_ID = "_legend";
// static callback to generate legend into HTML
private static final HtmlLegendLabelsCallback CALLBACK = new HtmlLegendLabelsCallback();
// cache to store options in order do not load every time the options
private final Map pluginOptions = new HashMap<>();
// cache to store legend items managed by chart
private final Map> pluginLegendLabelsItems = new HashMap<>();
// cache to store the chart id in order to know when new legend must be created
private final Set pluginAddedLegendStatus = new HashSet<>();
// cache to store DIV element which contains legend for each chart
private final Map pluginDivElements = new HashMap<>();
// cache to store easing during drawing for each chart
// this cache is needed in order to recreate the legend when a chart update
// is invoked during a previous update
private final Map pluginEasingStatus = new HashMap<>();
// cache to store the original value of legend in order to
// manage the change of the legend display after chart creation
private final Map pluginLegendDisplayStatus = new HashMap<>();
// cache to store the callback proxies of legend
private final Map pluginCallbackProxies = new HashMap<>();
/**
* To avoid any instantiation
*/
private HtmlLegend() {
// do nothing
}
/**
* Returns the singleton instance of plugin.
*
* @return the singleton instance of plugin
*/
public static HtmlLegend get() {
return INSTANCE;
}
/*
* (non-Javadoc)
*
* @see org.pepstock.charba.client.Plugin#getId()
*/
@Override
public String getId() {
return ID;
}
/*
* (non-Javadoc)
*
* @see org.pepstock.charba.client.plugins.AbstractPlugin#onConfigure(org.pepstock.charba.client. AbstractChart)
*/
@Override
public void onConfigure(IsChart chart) {
// checks if argument is consistent
if (IsChart.isConsistent(chart)) {
// adds into a map the callback proxy instance
// to catch events form legend
if (!pluginCallbackProxies.containsKey(chart.getId())) {
pluginCallbackProxies.put(chart.getId(), new HtmlLegendCallbackProxy());
}
HtmlLegendOptions pOptions = null;
// loads chart options for the chart
IsDefaultScaledOptions options = chart.getWholeOptions();
// if not, loads and cache
// creates the plugin options using the java script object
// passing also the default color set at constructor.
if (options.getPlugins().hasOptions(ID)) {
pOptions = options.getPlugins().getOptions(ID, FACTORY);
} else {
pOptions = new HtmlLegendOptions(HtmlLegendDefaultsOptions.DEFAULTS_INSTANCE);
}
pluginOptions.put(chart.getId(), pOptions);
pOptions.setCurrentCursor(chart.getInitialCursor());
// checks if the plugin is configured to show legend
if (pOptions.isDisplay()) {
// if the legend is set do not display
// or the OOTB legend plugin has been disable
// it respects it then ignore it and the plugin in
// will be disable
manageLegendDisplay(chart, pOptions);
} else {
// resets status of plugin
// because display is false
resetStatus(chart);
}
}
}
/*
* (non-Javadoc)
*
* @see org.pepstock.charba.client.plugins.AbstractPlugin#onBeforeUpdate(org.pepstock.charba.client.IsChart)
*/
@Override
public boolean onBeforeUpdate(IsChart chart) {
// checks if argument is consistent
if (mustBeDisplay(chart)) {
// gets the legend
Legend legend = chart.getOptions().getLegend();
// creates legend DIV element reference
Div legendElement = null;
// checks if element is alreayd created
if (!pluginDivElements.containsKey(chart.getId())) {
// if new
// creates a DIV element
legendElement = DOMBuilder.get().createDivElement();
// sets the id by chart instance
legendElement.setId(formatLegendElementId(chart));
// stores into map
pluginDivElements.put(chart.getId(), legendElement);
} else {
// if here, DIV element already exists then it retrieves it
legendElement = pluginDivElements.get(chart.getId());
}
// checks if there is a parent
if (legendElement.getParentNode() == null) {
// if no parent, means new object to add to chart element
addLegendElement(chart.getChartElement(), legendElement, legend.getPosition(), legend.getLabels().getPadding());
} else {
// removes the item
// in order to after draw to create the legend
pluginAddedLegendStatus.remove(chart.getId());
// if here, the div has got the parent
// then it checks if the position is the same when it has been created
// otherwise it will move to the right position
manageLegendElement(chart, legendElement, legend.getPosition());
}
// checks if is full width has been set
if (legend.isFullWidth()) {
// sets 100% of width
legendElement.getStyle().setWidth(Unit.PCT.format(100));
}
}
return true;
}
/*
* (non-Javadoc)
*
* @see org.pepstock.charba.client.plugins.AbstractPlugin#onBeforeDraw(org.pepstock.charba.client.IsChart, double)
*/
@Override
public boolean onBeforeDraw(IsChart chart, double easing) {
// checks if argument is consistent
// checks if reloading during previous drawing
// if the previous stored easing is greater than the current one her
// means that the is chart is updating without waiting for ending
// the previous update
if (mustBeDisplay(chart) && pluginEasingStatus.put(chart.getId(), easing) > easing) {
// removes the item
// in order to after draw to create the legend
pluginAddedLegendStatus.remove(chart.getId());
}
return true;
}
/*
* (non-Javadoc)
*
* @see org.pepstock.charba.client.plugins.AbstractPlugin#onAfterDraw(org.pepstock.charba.client. AbstractChart, double)
*/
@Override
public void onAfterDraw(IsChart chart, double easing) {
// checks if argument is consistent
if (mustBeDisplay(chart)) {
// checks if the legend must be created
// the legend will be created if there is the legend element
// and the chart is is NOT in the set
if (pluginDivElements.containsKey(chart.getId()) && !pluginAddedLegendStatus.contains(chart.getId())) {
// gets div element
Div legendElement = pluginDivElements.get(chart.getId());
// invokes the legend callback to have the HTML of legend
SafeHtml html = CALLBACK.generateLegend(chart);
// removes all children of div element
legendElement.removeAllChildren();
// sets as inner HTML
legendElement.setInnerHTML(html.asString());
// removes all listeners
removeListeners(chart, legendElement);
// adds the event listeners to element
addListeners(chart, legendElement);
// adds into set
// in order do not add the inner html every easing
pluginAddedLegendStatus.add(chart.getId());
}
// if end of drawing,
// removes charts from set
if (easing == 1D) {
// removes chart for items
// in order to add next cycle
pluginAddedLegendStatus.remove(chart.getId());
// sets easing to zero
pluginEasingStatus.put(chart.getId(), 0D);
}
}
}
/*
* (non-Javadoc)
*
* @see org.pepstock.charba.client.plugins.AbstractPlugin#onDestroy(org.pepstock.charba.client.IsChart)
*/
@Override
public void onDestroy(IsChart chart) {
// checks if argument is consistent
if (IsChart.isValid(chart)) {
// resets all status items
resetStatus(chart);
// removes status of legend display
pluginLegendDisplayStatus.remove(chart.getId());
// removes callback proxies
pluginCallbackProxies.remove(chart.getId());
// removes the chart from options
HtmlLegendOptions oldOptions = pluginOptions.remove(chart.getId());
// scans all options to see if the options is used in another chart
for (HtmlLegendOptions options : pluginOptions.values()) {
// checks if the option si s equals to old one
if (options.getCharbaId() == oldOptions.getCharbaId()) {
return;
}
}
// if here, the old options is no longer used
// then it removes the legend callback from cache
FACTORY.store(oldOptions.getCharbaId(), null);
}
}
/**
* Manages if the legend must be displayed or not based on choice of user and what can be changed into chart configuration after the chart initialization bu a
* chart.reconfigure
.
*
* @param chart chart instance to manage
* @param pOptions plugin option for the chart
*/
private void manageLegendDisplay(IsChart chart, HtmlLegendOptions pOptions) {
// if the legend is set do not display
// or the OOTB legend plugin has been disable
// it respects it then ignore it and the plugin in
// will be disable
boolean cachedValue = false;
// check if the display must be stored because was changed by user
if (pluginLegendDisplayStatus.containsKey(chart.getId())) {
// if here the plugin was already initialized
// and then it stored the display value
cachedValue = pluginLegendDisplayStatus.get(chart.getId());
// because is not the first round
// the legend value should be false because set by plugin
// if is true, means the user changed it programmatically
if (chart.getOptions().getLegend().isDisplay() && !cachedValue) {
// stored the legend display value because is changed
pluginLegendDisplayStatus.put(chart.getId(), chart.getOptions().getLegend().isDisplay());
}
} else {
// stored the legend display value because is missing
pluginLegendDisplayStatus.put(chart.getId(), chart.getOptions().getLegend().isDisplay());
}
boolean mustBeChecked = chart.getOptions().getLegend().isDisplay() || cachedValue;
if (mustBeChecked && !chart.getOptions().getPlugins().isForcedlyDisabled(DefaultPlugin.LEGEND)) {
// disable legend
chart.getOptions().getLegend().setDisplay(false);
// sets legend callback
// overriding whatever other callback has been set
chart.getOptions().setLegendCallback(CALLBACK);
// sets easing to zero
pluginEasingStatus.put(chart.getId(), 0D);
// creates options instance
} else {
// resets all status items if there are
// this is in case of update options
resetStatus(chart);
// disables display of plugin
pOptions.setDisplay(false);
}
}
/**
* Returns true
if the plugin must be manage the legend, creating it.
*
* @param chart chart instance.
* @return true
if the plugin must be manage the legend, creating it.
*/
private boolean mustBeDisplay(IsChart chart) {
// checks if argument is consistent
if (IsChart.isValid(chart) && pluginOptions.containsKey(chart.getId())) {
// gets stored option
HtmlLegendOptions options = pluginOptions.get(chart.getId());
// returns if must be display
return options.isDisplay();
}
// if here, chart is not consistent or no option
// then returns false
return false;
}
/**
* Removes all status items from all maps.
*
* @param chart chart instance
*/
private void resetStatus(IsChart chart) {
// checks if there is div element for legend
if (pluginDivElements.containsKey(chart.getId())) {
// removes from map
Div legendElement = pluginDivElements.remove(chart.getId());
// removes all listeners
removeListeners(chart, legendElement);
// removes all children
legendElement.removeAllChildren();
// removes from parent
legendElement.removeFromParent();
}
// removes the chart status
pluginAddedLegendStatus.remove(chart.getId());
// removes the chart legend labels items
pluginLegendLabelsItems.remove(chart.getId());
// removes the chart from easing status
pluginEasingStatus.remove(chart.getId());
// removes cached point style from tile factory
// if there are
HtmlLegendItem htmlLegendItem = new HtmlLegendItem(chart);
TilesFactory.clearHtmlLegendItems(htmlLegendItem);
}
/**
* Creates a HTML element id for chart legend.
* The format is:
* [chartId]-legend
*
* @param chart chart instance
* @return a string representation of HTML element id.
*/
private String formatLegendElementId(IsChart chart) {
return chart.getId() + SUFFIX_LEGEND_ELEMENT_ID;
}
/**
* Adds the event listeners to all elements created by legend callback.
*
* @param chart chart instance
* @param legendElement DIV legend element which contains the custom HTML legend.
*/
private void addListeners(IsChart chart, Div legendElement) {
// gets all nodes with TAG TD
NodeList tds = legendElement.getElementsByTagName(TableCell.TAG);
// scans all nodes
for (int i = 0; i < tds.length(); i++) {
// gets element
BaseElement td = tds.item(i);
// checks if the element has got a correct ID
// starting with chart id
if (td.getId().startsWith(chart.getId()) && pluginCallbackProxies.containsKey(chart.getId())) {
HtmlLegendCallbackProxy callbackProxy = pluginCallbackProxies.get(chart.getId());
// adds to the element all event listeners
callbackProxy.addListeners(td);
}
}
}
/**
* Removes the event listeners from all elements created by legend callback.
*
* @param chart chart instance
* @param legendElement DIV legend element which contains the custom HTML legend.
*/
private void removeListeners(IsChart chart, Div legendElement) {
// gets all nodes with TAG TD
NodeList tds = legendElement.getElementsByTagName(TableCell.TAG);
// scans all nodes
for (int i = 0; i < tds.length(); i++) {
// gets element
BaseElement td = tds.item(i);
// checks if the element has got a correct ID
// starting with chart id
if (td.getId().startsWith(chart.getId()) && pluginCallbackProxies.containsKey(chart.getId())) {
HtmlLegendCallbackProxy callbackProxy = pluginCallbackProxies.get(chart.getId());
// removes to the element all event listeners
callbackProxy.removeListeners(td);
}
}
}
/**
* Adds the HTML legend element to the right position into chart element, depending on {@link Position} set for legend.
*
* @param chartElement chart HTML element
* @param legendElement legend HTML element
* @param position position set by legend configuration object
* @param padding padding set by legend configuration object
*/
private void addLegendElement(Div chartElement, Div legendElement, Position position, int padding) {
if (mustAddToBottom(position)) {
// appends the legend element
chartElement.appendChild(legendElement);
// and sets the bottom padding
legendElement.getStyle().setPaddingBottom(Unit.PX.format(padding));
} else {
// if not bottom, inserts the legend element as first
chartElement.insertBefore(legendElement, chartElement.getFirstChild());
// and sets the top padding
legendElement.getStyle().setPaddingTop(Unit.PX.format(padding));
}
}
/**
* Manages the HTML legend element to the right position into chart element, depending on {@link Position} set for legend.
* This method is called when a legend element is already added and the position could be changed comparing when the element has been created.
*
* @param chart chart instance
* @param legendElement legend HTML element
* @param position position set by legend configuration object
*/
private void manageLegendElement(IsChart chart, Div legendElement, Position position) {
// gets chart element
Div chartElement = chart.getChartElement();
// gets if the legend element has been defined after the canvas
boolean isAfterCanvas = isAfterCanvas(chart, legendElement);
// gets if the legend must be added to bottom
boolean mustBeAddedToBottom = mustAddToBottom(position);
if (mustBeAddedToBottom && !isAfterCanvas) {
// removes the legend element from parent
legendElement.removeFromParent();
// and appends at the end
chartElement.appendChild(legendElement);
} else if (!mustBeAddedToBottom && isAfterCanvas) {
// if here the position is not bottom but
// legend element is after the canvas
// then it removes from parent
legendElement.removeFromParent();
// and inserts it at the beginning
chartElement.insertBefore(legendElement, chartElement.getFirstChild());
}
}
/**
* Checks if the legend must be added into chart element on top or bottom.
*
* @param position position set by legend configuration object
* @return true
if the legend must be added to bottom
*/
private boolean mustAddToBottom(Position position) {
// if position is bottom or right
// legend is to bottom
return Position.RIGHT.equals(position) || Position.BOTTOM.equals(position);
}
/**
* Returns true
if the legend element has been added to chart element after the canvas one.
*
* @param chart chart instance
* @param legendElement legend HTML element
* @return true
if the legend element has been added to chart element after the canvas one
*/
private boolean isAfterCanvas(IsChart chart, Div legendElement) {
// gets chart element
Div chartElement = chart.getChartElement();
// retrieves canvas id of chart
String canvasId = chart.getCanvas().getId();
// scans all children of chart element
NodeList children = chartElement.getChildNodes();
for (int i = 0; i < children.length(); i++) {
// gets the node
BaseNode childNode = children.item(i);
// checks if is html element
if (childNode instanceof BaseHtmlElement) {
BaseHtmlElement childElement = (BaseHtmlElement) childNode;
// checks if the legend element is equals to the scanned node
// by its id
if (childElement.getNodeName().equalsIgnoreCase(Div.TAG) && legendElement.getId().equalsIgnoreCase(childElement.getId())) {
// if here, means that the legend element has been found before the canvas element
// then returns false
return false;
} else if (childElement.getNodeName().equalsIgnoreCase(Canvas.TAG) && childElement.getId().equalsIgnoreCase(canvasId)) {
// if here, means that the canvas element has been found before the legend element
// then returns true
return true;
}
}
}
// if here, nothing was found then return false
// it should not happen
return false;
}
/**
* Returns the map of cached plugin options.
*
* @return the map of cached plugin options
*/
Map getPluginOptions() {
return pluginOptions;
}
/**
* Returns the map of cached legend labels items.
*
* @return the map of cached legend labels items
*/
Map> getPluginLegendLabelsItems() {
return pluginLegendLabelsItems;
}
/**
* Returns the map of cached added legend labels status.
*
* @return the map of cached added legend labels status
*/
Set getPluginAddedLegendStatus() {
return pluginAddedLegendStatus;
}
/**
* Returns the map of cached DIV elements of HTML legend.
*
* @return the map of cached DIV elements of HTML legend
*/
Map getPluginDivElements() {
return pluginDivElements;
}
}