296 changes: 296 additions & 0 deletions processing/dataplotly_algorithms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
# -*- coding: utf-8 -*-
"""
/***************************************************************************
DataPlotly
A QGIS plugin
D3 Plots for QGIS
-------------------
begin : 2017-03-05
git sha : $Format:%H$
copyright : (C) 2017 by matteo ghetta
email : matteo.ghetta@gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""

from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
from DataPlotly.data_plotly_plot import *

from qgis.utils import plugins
from qgis.core import (
QgsProcessingUtils,
QgsProcessingException,
QgsProcessingAlgorithm,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterField,
QgsProcessingParameterString,
QgsProcessingParameterEnum,
QgsProcessingParameterFileDestination,
QgsProcessingOutputHtml,
QgsProcessingOutputFile,
QgsSettings,
QgsFeatureRequest

)

from qgis.PyQt.QtCore import Qt, QCoreApplication

from processing.tools import vector

from shutil import copyfile
import json, codecs
class DataPlotlyProcessingPlot(QgisAlgorithm):

"""
Create a simple plot with DataPlotly plugin
"""

# Constants used to refer to parameters and outputs. They will be
# used when calling the algorithm from another algorithm, or when
# calling from the QGIS console.

INPUT = 'INPUT'
PLOT_TYPE = 'PLOT_TYPE'
PLOT_TITLE = 'PLOT_TITLE'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we have to let this parameter optional

PLOT_TYPE_OPTIONS = ['scatter', 'box', 'bar', 'histogram', 'pie', '2dhistogram', 'polar', 'contour']
X_MANDATORY = ['scatter', 'bar', 'histogram', '2dhistogram', 'polar', 'contour']
Y_MANDATORY = ['scatter', 'box', 'bar', 'pie', '2dhistogram', 'polar', 'contour']
XFIELD = 'XFIELD'
YFIELD = 'YFIELD'
IN_COLOR = 'IN_COLOR'
IN_COLOR_OPTIONS = ['Black', 'Blue', 'Brown', 'Cyan', 'DarkBlue', 'Grey', 'Green', 'LightBlue', 'Lime', 'Magenta', 'Maroon', 'Olive', 'Orange', 'Purple', 'Red', 'Silver', 'White', 'Yellow']
IN_COLOR_HTML = 'IN_COLOR_HTML'
OUTPUT_HTML_FILE = 'OUTPUT_HTML_FILE'
OUTPUT_JSON_FILE = 'OUTPUT_JSON_FILE'

def __init__(self):
super().__init__()
"""Here we define the inputs and output of the algorithm, along
with some other properties.
"""

@staticmethod
def tr(string, context=''):
if context == '':
context = 'Processing'
return QCoreApplication.translate(context, string)

def initAlgorithm(self, config=None):

self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT,
self.tr('Input layer')
)
)

self.addParameter(
QgsProcessingParameterEnum(
self.PLOT_TYPE,
self.tr('Plot type'),
options=self.PLOT_TYPE_OPTIONS
)
)

self.addParameter(
QgsProcessingParameterString(
self.PLOT_TITLE,
self.tr('Plot title'),
optional=True
)
)

self.addParameter(
QgsProcessingParameterField(
self.XFIELD,
self.tr('X Field'),
parentLayerParameterName=self.INPUT,
type=QgsProcessingParameterField.Any,
optional=True
)
)

self.addParameter(
QgsProcessingParameterField(
self.YFIELD,
self.tr('Y Field'),
parentLayerParameterName=self.INPUT,
type=QgsProcessingParameterField.Any,
optional=True
)
)

self.addParameter(
QgsProcessingParameterEnum(
self.IN_COLOR,
self.tr('Color'),
optional=True,
options=self.IN_COLOR_OPTIONS
)
)

self.addParameter(
QgsProcessingParameterString(
self.IN_COLOR_HTML,
self.tr('Color (any valid HTML color) If set, this is used instead of the color set in the previous input.'),
optional=True
)
)

self.addParameter(
QgsProcessingParameterFileDestination(self.OUTPUT_HTML_FILE,
self.tr('HTML File'),
self.tr('HTML files (*.html)')
)
)
# need to add an output for it to allow to see it in Processus results viewer
self.addOutput(QgsProcessingOutputHtml(self.OUTPUT_HTML_FILE, self.tr('Dataplotly - Generic plot HTML output')))

# Add an file to return a response in JSON format
self.addParameter(
QgsProcessingParameterFileDestination(self.OUTPUT_JSON_FILE,
self.tr('JSON file'),
self.tr('JSON Files (*.json)')
)
)
# need to add an output for it to allow to see it in Processus results viewer
self.addOutput(QgsProcessingOutputFile(self.OUTPUT_JSON_FILE, self.tr('Dataplotly - Generic plot JSON output')))


def name(self):
# Unique (non-user visible) name of algorithm
return 'build_generic_plot'

def displayName(self):
# The name that the user will see in the toolbox
return self.tr('Build a generic plot')

def group(self):
return self.tr('Plots')

def processAlgorithm(self, parameters, context, feedback):
"""
:param parameters:
:param context:
"""

layer = self.parameterAsSource(parameters, self.INPUT, context)
fields = layer.fields()

xfieldname = self.parameterAsString(parameters, self.XFIELD, context)
yfieldname = self.parameterAsString(parameters, self.YFIELD, context)

outputHtmlFile = self.parameterAsFileOutput(parameters, self.OUTPUT_HTML_FILE, context)
outputJsonFile = self.parameterAsFileOutput(parameters, self.OUTPUT_JSON_FILE, context)

plot_type = 'bar'
plot_type_input = self.parameterAsInt(parameters, self.PLOT_TYPE, context)
plot_type = self.PLOT_TYPE_OPTIONS[plot_type_input]

plot_title = self.parameterAsString(parameters, self.PLOT_TITLE, context)

in_color_input = self.parameterAsInt(parameters, self.IN_COLOR, context)
in_color_hex = self.IN_COLOR_OPTIONS[in_color_input]
in_color_html = self.parameterAsString(parameters, self.IN_COLOR_HTML, context)

# Some controls
msg = []
if plot_type in self.X_MANDATORY and not xfieldname:
msg.append(self.tr("The chosen plot type needs a X field !"))
if plot_type in self.Y_MANDATORY and not yfieldname:
msg.append(self.tr("The chosen plot type needs a Y field !"))
if msg:
feedback.reportError(' '.join(msg))
raise QgsProcessingException(msg)

# Build needed dictionary
pdic = {}
pdic['plot_type'] = plot_type
pdic['plot_prop'] = {}

# Add X dimension
if xfieldname:
# get field index for x
idxX = layer.fields().lookupField(xfieldname)
# get list of values for x
x_var = [i[xfieldname] for i in layer.getFeatures(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([idxX]))]
fieldTypeX = fields[idxX].type()
x_title = fields[idxX].alias() or xfieldname
pdic['plot_prop']['x'] = x_var

# Add Y dimension
if yfieldname:
# get field index for y
idxY = layer.fields().lookupField(yfieldname)
# get list of values for y
y_var = [i[yfieldname] for i in layer.getFeatures(QgsFeatureRequest().setFlags(QgsFeatureRequest.NoGeometry).setSubsetOfAttributes([idxY]))]
y_title = fields[idxY].alias() or yfieldname
pdic['plot_prop']['y'] = y_var


# Draw only markers for scatter plot
if plot_type in ['scatter', 'polar']:
pdic['plot_prop']['marker'] = 'markers'

# Colours
pdic['plot_prop']['in_color'] = in_color_html or in_color_hex or 'DodgerBlue'

# Add layout
pdic['layout_prop'] = {
'title': plot_title or layer.sourceName()
}
if plot_type in self.X_MANDATORY:
pdic['layout_prop']['x_title'] = x_title
if plot_type in self.Y_MANDATORY:
pdic['layout_prop']['y_title'] = y_title

# Add layer
pdic['layer'] = layer

# Create plot instance
plot_instance = Plot(
pdic['plot_type'],
pdic["plot_prop"],
pdic["layout_prop"]
)

# Initialize plot properties and build them
trace = plot_instance.buildTrace()

# Initialize layout properties and build them
layout = plot_instance.buildLayout()

# Prepare results
results = {
self.OUTPUT_HTML_FILE: None,
self.OUTPUT_JSON_FILE: None
}

# Save plot as HTML
if outputHtmlFile:
standalone_plot_path = plot_instance.buildFigure()
if os.path.isfile(standalone_plot_path):
# html file output
copyfile(standalone_plot_path, outputHtmlFile)
results[self.OUTPUT_HTML_FILE] = outputHtmlFile


# Save plot as JSON
if outputJsonFile:
ojson = {
'data': trace,
'layout': layout
}
with codecs.open(outputJsonFile, 'w', encoding='utf-8') as f:
f.write(json.dumps(ojson))
results[self.OUTPUT_JSON_FILE] = outputJsonFile

return results

108 changes: 108 additions & 0 deletions processing/dataplotly_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# -*- coding: utf-8 -*-
"""
/***************************************************************************
DataPlotly
A QGIS plugin
D3 Plots for QGIS
-------------------
begin : 2017-03-05
git sha : $Format:%H$
copyright : (C) 2017 by matteo ghetta
email : matteo.ghetta@gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
from PyQt5.QtGui import QIcon
from qgis.core import QgsProcessingProvider, QgsApplication
from processing.core.ProcessingConfig import Setting, ProcessingConfig

from .dataplotly_algorithms import DataPlotlyProcessingPlot

class DataPlotlyProvider(QgsProcessingProvider):

MY_DUMMY_SETTING = 'MY_DUMMY_SETTING'

def __init__(self):
super().__init__()

self.algs = []

def getAlgs(self):
algs = [DataPlotlyProcessingPlot()]

return algs

def load(self):
"""In this method we add settings needed to configure our
provider.
"""
ProcessingConfig.settingIcons[self.name()] = self.icon()
# Deactivate provider by default
ProcessingConfig.addSetting(Setting(self.name(), 'ACTIVATE_EXAMPLE',
'Activate', False))
ProcessingConfig.addSetting(Setting('Example algorithms',
DataPlotlyProvider.MY_DUMMY_SETTING,
'Example setting', 'Default value'))
ProcessingConfig.readSettings()
self.refreshAlgorithms()
return True

def unload(self):
"""Setting should be removed here, so they do not appear anymore
when the plugin is unloaded.
"""
ProcessingConfig.removeSetting('ACTIVATE_EXAMPLE')
ProcessingConfig.removeSetting(
DataPlotlyProvider.MY_DUMMY_SETTING)

def isActive(self):
"""Return True if the provider is activated and ready to run algorithms"""
return ProcessingConfig.getSetting('ACTIVATE_EXAMPLE')

def setActive(self, active):
ProcessingConfig.setSettingValue('ACTIVATE_EXAMPLE', active)

def id(self):
"""This is the name that will appear on the toolbox group.
It is also used to create the command line name of all the
algorithms from this provider.
"""
return 'DataPlotly'

def name(self):
"""This is the localised full name.
"""
return 'DataPlotly'

def icon(self):
"""We return the default icon.
"""
return QIcon(":/plugins/DataPlotly/icon.png")

def loadAlgorithms(self):
"""Here we fill the list of algorithms in self.algs.
This method is called whenever the list of algorithms should
be updated. If the list of algorithms can change (for instance,
if it contains algorithms from user-defined scripts and a new
script might have been added), you should create the list again
here.
In this case, since the list is always the same, we assign from
the pre-made list. This assignment has to be done in this method
even if the list does not change, since the self.algs list is
cleared before calling this method.
"""
self.algs = self.getAlgs()
for a in self.algs:
self.addAlgorithm(a)