org.wicketstuff.gchart.Chart.java Source code

Java tutorial

Introduction

Here is the source code for org.wicketstuff.gchart.Chart.java

Source

/* 
 * 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.wicketstuff.gchart;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.apache.wicket.Component;
import org.apache.wicket.Session;
import org.apache.wicket.markup.head.HeaderItem;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.JavaScriptContentHeaderItem;
import org.apache.wicket.markup.head.JavaScriptHeaderItem;
import org.apache.wicket.markup.head.JavaScriptReferenceHeaderItem;
import org.apache.wicket.markup.html.WebComponent;
import org.apache.wicket.model.IComponentAssignedModel;
import org.apache.wicket.model.IModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wicketstuff.gchart.gchart.options.ChartOptions;

import com.github.openjson.JSONArray;
import com.github.openjson.JSONObject;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;

/**
 * Abstraction of Google charts for wicket. {@code OutputMarkupId} is set to
 * true, since id is referenced in generated JavaScript.
 *
 * @author Dieter Tremel
 */
public class Chart extends WebComponent implements JavaScriptable, Jsonable {

    private static final long serialVersionUID = 1L;
    private static final Logger log = LoggerFactory.getLogger(Chart.class);
    /** URL for Google lib loader */
    public static final String LOADER_URL = "https://www.gstatic.com/charts/loader.js";

    private ChartLibLoader loader = null;
    private boolean responsive = true;
    private Locale locale = null;
    private String mapsApiKey = null;
    private IModel<ChartType> typeModel;
    private IModel<DataTable> dataModel;

    /**
     * Basic Constructor.
     *
     * @param id Wicket id. Id is used in javascript, but at the moment not
     * escaped to JavaScript identifier rules. So use only characters allowed
     * for JavaScript declarations to avoid problems.
     */
    public Chart(String id) {
        super(id);
        setOutputMarkupId(true);
    }

    /**
     * Complete Constructor.
     *
     * @param id Wicket id. Id is used in javascript, but at the moment not
     * escaped to JavaScript identifier rules. So use only characters allowed
     * for JavaScript declarations to avoid problems.
     *
     * This constructor is for use without {@link ChartLibLoader} (one chart per
     * page).
     *
     * @param typeModel Model of Type of chart.
     * @param optionModel Model of optionModel.
     * @param dataModel Model of data dataModel.
     */
    public Chart(String id, IModel<ChartType> typeModel, IModel<ChartOptions> optionModel,
            IModel<DataTable> dataModel) {
        this(id, typeModel, optionModel, dataModel, null);
    }

    /**
     * Complete Constructor.
     *
     * @param id Wicket id. Id is used in javascript, but at the moment not
     * escaped to JavaScript identifier rules. So use only characters allowed
     * for JavaScript declarations to avoid problems.
     *
     * This constructor is for use with {@link ChartLibLoader} (multiple charts
     * per page).
     *
     * @param typeModel Model of Type of chart.
     * @param optionModel Model of optionModel.
     * @param dataModel Model of data dataModel.
     * @param loader Loader to add the chart to.
     */
    public Chart(String id, IModel<ChartType> typeModel, IModel<ChartOptions> optionModel,
            IModel<DataTable> dataModel, ChartLibLoader loader) {
        this(id);
        this.typeModel = typeModel;
        this.setDefaultModel(optionModel);
        setDataModel(dataModel);
        //        add(new GoogleChartBehavior(this, getMarkupId()));
        this.loader = loader;
        if (loader != null) {
            loader.addChart(this);
        }
    }

    @Override
    protected void onDetach() {
        super.onDetach();
        typeModel.detach();
        dataModel.detach();
    }

    /**
     * Get Google Loader URL. Can be overwritten, if Google changes URL in
     * future.
     *
     * @return Returns {@link #LOADER_URL}.
     */
    public String getLoaderUrl() {
        return LOADER_URL;
    }

    /**
     * Create the JavaScript for the Google loader.
     *
     * @return HeaderItem for Google loader url.
     */
    public JavaScriptHeaderItem createLoaderItem() {
        return JavaScriptHeaderItem.forUrl(getLoaderUrl());
    }

    @Override
    public void renderHead(final IHeaderResponse response) {
        super.renderHead(response);
        // TODO ist this rendering sufficient for refreshing chart by AJAX?

        final List<HeaderItem> depItemList = new ArrayList<>();
        final JavaScriptContentHeaderItem chartScriptItem = new JavaScriptContentHeaderItem(toJavaScript(),
                getScriptId(), null) {
            private static final long serialVersionUID = 1L;

            @Override
            public List<HeaderItem> getDependencies() {
                return depItemList;
            }
        };
        if (loader == null) {
            response.render(createLoaderItem());
        } else {
            depItemList.add(loader.getHeaderItem());
        }
        response.render(chartScriptItem);
        if (responsive) {
            final JavaScriptReferenceHeaderItem jQueryHeaderItem = JavaScriptHeaderItem
                    .forReference(getApplication().getJavaScriptLibrarySettings().getJQueryReference());
            //            response.render(jQueryHeaderItem);
            response.render(new JavaScriptContentHeaderItem(createRedrawJavaScript(), getRedrawScriptId(), null) {
                private static final long serialVersionUID = 1L;

                @Override
                public List<HeaderItem> getDependencies() {
                    final List<HeaderItem> dependencies = super.getDependencies();
                    dependencies.add(jQueryHeaderItem);
                    return dependencies;
                }
            });
        }
    }

    /**
     * Configure an ajax response to redraw the chart.
     * Use this call for instance in {@code AjaxCheckBox#onUpdate} or 
     * {@code AjaxLink#onClick}.
     * Can be used after data change or options change.
     * See example page for usage example switching StackedPercent option on a bar chart.
     * 
     * @param target Request target to configure.
     */
    public void configureAjaxUpdate(AjaxRequestTarget target) {
        target.getHeaderResponse().render(new OnDomReadyHeaderItem(toJavaScript()));
        target.appendJavaScript(getCallbackId() + "();");
    }

    /**
     * Create the Javascript HeaderItem for the chart without dependencies for ajax.
     *
     * @return Header item for chart draw script.
     */
    public JavaScriptContentHeaderItem getJavaScriptHeaderItem() {
        return new JavaScriptContentHeaderItem(toJavaScript(), getScriptId(), null);
    }

    /**
     * Should some JavaScript be added to make the Chart responsive. By default
     * Google charts are not responsive, this is an extension by wicket-gchart.
     *
     * <p>
     * This is set to true by default.
     *
     * @return True if the chart will redraw on window resize, false if not.
     */
    public boolean isResponsive() {
        return responsive;
    }

    /**
     * Set this to true if some JavaScript should be added to make the Chart
     * responsive. By default Google charts are not responsive, this is an
     * extension by wicket-gchart. If true a script like
     * <pre>
     * {@code $(window).resize(function(){
     *   drawChart1();
     * });}
     * </pre> will be rendered in head to add this functionality.
     *
     * @param responsive Set to true to dynamically redraw the chart on resize
     * events. Set to false not to redraw. Default is true.
     */
    public void setResponsive(boolean responsive) {
        this.responsive = responsive;
    }

    /**
     * The Locale is used to render a {@code language = "de"} in the
     * <a href="https://developers.google.com/chart/interactive/docs/basic_load_libs">load
     * statement</a> like in the example:
     * <pre>
     * {@code
     * // Load Google Charts for the Japanese locale.
     * google.charts.load('current', {'packages':['corechart'], 'language': 'ja'});
     * }</pre> If Locale is not explicitly set and null, the wicket {@link Component#getLocale()
     * }
     * is returned, which is identical to {@link Session#getLocale() }. To
     * override use {@link #setLocale(java.util.Locale) }.
     *
     * @return Locale used for the package language option.
     */
    @Override
    public Locale getLocale() {
        if (locale == null) {
            return super.getLocale();
        } else {
            return locale;
        }
    }

    /**
     * Override default Locale for the chart. See {@link #getLocale() }.
     *
     * @param locale Locale to render chart with.
     */
    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    /**
     * Getter for Google Maps API key for geo- and map charts. In all other
     * charts this value will be null. Getter should never find any usage except
     * tests and logging.
     *
     * @return Google Maps API key. Null except for Geo Charts.
     */
    public String getMapsApiKey() {
        return mapsApiKey;
    }

    /**
     * Setter for Google Maps API key for geo- and map charts. In all other
     * charts this has no use. For geo- and mapcharts add this key to be
     * rendered in package deklaration:
     * <pre> {@code
     * google.charts.load('current', {
     *  'packages':['geochart'],
     *  // Note: you will need to get a mapsApiKey for your project.
     *  // See: https://developers.google.com/chart/interactive/docs/basic_load_libs#load-settings
     *  'mapsApiKey': 'AIzaSyD-9tSrke72PouQMnMX-a7eZSW0jkFMBWY'
     * });
     * }</pre>
     *
     * @param mapsApiKey Google Maps API key.
     */
    public void setMapsApiKey(String mapsApiKey) {
        this.mapsApiKey = mapsApiKey;
    }

    public IModel<ChartType> getTypeModel() {
        return typeModel;
    }

    public void setTypeModel(IModel<ChartType> typeModel) {
        this.typeModel = typeModel;
    }

    public IModel<ChartOptions> getOptionModel() {
        return (IModel<ChartOptions>) getDefaultModelObject();
    }

    public void setOptionModel(IModel<ChartOptions> optionModel) {
        this.setDefaultModel(optionModel);
    }

    public IModel<DataTable> getDataModel() {
        return dataModel;
    }

    public final void setDataModel(IModel<DataTable> dataModel) {
        this.dataModel = dataModel instanceof IComponentAssignedModel
                ? ((IComponentAssignedModel<DataTable>) dataModel).wrapOnAssignment(this)
                : dataModel;
    }

    /**
     * Create the load statement of the chart lib with package, language and
     * Maps API key declaration as defined by the chart and its
     * {@link ChartType}.
     *
     * @return Onle line load statement to be included in a JavaScript.
     */
    private String createLoaderStatement() {
        StringBuilder sb = new StringBuilder();
        // Load the Visualization API and the package.
        JSONObject packageDecl = new JSONObject();
        JSONArray packages = new JSONArray();
        packages.put(typeModel.getObject().getLoadPackage());
        packageDecl.put("packages", packages);
        packageDecl.put("language", getLocale().getLanguage());
        if (mapsApiKey != null) {
            packageDecl.put("mapsApiKey", mapsApiKey);
        }
        sb.append("google.charts.load('current', ").append(packageDecl.toString()).append(");").append("\n");
        return sb.toString();
    }

    @Override
    public String toJavaScript() {
        return createJavaScriptBuilder().toString();
    }

    /**
     * Create a StringBuilder for generating Javascript.
     * If needed the builder contains altready the chart library loader statement.
     *
     * @return Builder stub with loader statement.
     */
    protected StringBuilder initLoaderBuilder() {
        StringBuilder sb = new StringBuilder();

        if (loader == null) {
            sb.append(createLoaderStatement());
        }

        return sb;
    }

    /**
     * Create a builder containing the standard javascript without wrapper.
     *
     * @return Stringbuilder with already defined javascript.
     */
    protected StringBuilder createJavaScriptBuilder() {
        StringBuilder sb = initLoaderBuilder();

        // Register a callback to run when the Google Visualization API is loaded.
        sb.append("google.charts.setOnLoadCallback(").append(getCallbackId()).append(");").append("\n\n");

        // define callback function
        sb.append("function ").append(getCallbackId()).append("() {").append("\n");

        // data table
        final DataTable datatable = dataModel.getObject();
        sb.append(datatable.toJavaScript(getDataTableId())).append("\n");

        // options
        final ChartOptions options = (ChartOptions) getDefaultModelObject();
        sb.append(options.toJavaScript(getOptionsId())).append("\n");

        // Instantiate and draw our chart, passing in the optionModel.
        // var chart = new google.visualization.PieChart(document.getElementById('chart_div'));
        sb.append("var ").append(getChartId()).append(" = new ").append(typeModel.getObject().toJavaScript())
                .append("(");
        sb.append("document.getElementById('").append(getMarkupId()).append("')");
        sb.append(")").append("\n");

        // chart.draw(data, optionModel);
        sb.append(getChartId()).append(".draw(").append(getDataTableId()).append(", ").append(getOptionsId())
                .append(");").append("\n");

        sb.append("}").append("\n"); // close callback function

        return sb;
    }

    /**
     * Create a builder containing the javascript with use of ChartWrapper. See
     * <a href="https://developers.google.com/chart/interactive/docs/reference#chartwrapperobject">ChartWrapper
     * Class</a>.
     *
     * @return Stringbuilder with already defined javascript.
     */
    protected StringBuilder createWrapperJavaScriptBuilder() {
        StringBuilder sb = new StringBuilder();

        if (loader == null) {
            sb.append("google.charts.load('current');\n\n");
        }

        // Register a callback to run when the Google Visualization API is loaded.
        sb.append("google.charts.setOnLoadCallback(").append(getCallbackId()).append(");").append("\n\n");

        // define callback function
        sb.append("function ").append(getCallbackId()).append("() {").append("\n");

        // define wrapper
        sb.append("var wrapper = new google.visualization.ChartWrapper(");
        sb.append(toJSON());
        sb.append(");\n");

        // call draw
        sb.append("wrapper.draw();\n");

        sb.append("}").append("\n"); // close callback function

        return sb;
    }

    @Override
    public JSONObject toJSON() {
        JSONObject wrapperJason = new JSONObject();
        wrapperJason.put("chartType", typeModel.getObject().getChartClass());
        wrapperJason.put("dataTable", dataModel.getObject().toJSON());
        wrapperJason.put("options", ((ChartOptions) getDefaultModelObject()).toJSON());
        wrapperJason.put("containerId", getMarkupId());
        return wrapperJason;
    }

    /**
     * Make a callback identifier from chart id.
     *
     * @return Identifier for callback.
     */
    public String getCallbackId() {
        // TODO escape CSS ids in some way to avoid javascript id problems
        return "draw" + getId();
    }

    /**
     * Make a chart identifier from chart id.
     *
     * @return Identifier(js) for chart.
     */
    public String getChartId() {
        // TODO escape CSS ids in some way to avoid javascript id problems
        return getId() + "Chart";
    }

    /**
     * Make a script id from chart id.
     *
     * @return Identifier(js) for chart creation script.
     */
    public String getScriptId() {
        // TODO escape CSS ids in some way to avoid javascript id problems
        return getId() + "Script";
    }

    /**
     * Make a data table id from chart id.
     *
     * @return Identifier(js) for datatable variable.
     */
    public String getDataTableId() {
        // TODO escape CSS ids in some way to avoid javascript id problems
        return getId() + "DataTable";
    }

    /**
     * Make a options id from chart id.
     *
     * @return Identifier(js) for options variable.
     */
    public String getOptionsId() {
        // TODO escape CSS ids in some way to avoid javascript id problems
        return getId() + "Options";
    }

    /**
     * Make a script id for redraw script from chart id. This is used if chart
     * should be responsive.
     *
     * @return Identifier(js) for chart redraw script.
     */
    public String getRedrawScriptId() {
        // TODO escape CSS ids in some way to avoid javascript id problems
        return getId() + "RedrawScript";
    }

    //$(window).resize(function(){
    //  drawChart1();
    //  drawChart2();
    //});
    /**
     * Create a a redraw script for responsive charts. The chart is redrawn on
     * {@code (window).resize} events.
     *
     * @return Complete redraw script.
     */
    public String createRedrawJavaScript() {
        StringBuilder sb = new StringBuilder("$(window).resize(function(){");
        sb.append(getCallbackId()).append("();");
        sb.append("});");
        return sb.toString();
    }

}