def test_readwrite(self): """ Test reading and writing plot settings from XML """ doc = QDomDocument("properties") original = PlotSettings('test', properties={ 'marker_width': 2, 'marker_size': 5 }, layout={ 'title': 'my plot', 'legend_orientation': 'v' }) elem = original.write_xml(doc) self.assertFalse(elem.isNull()) res = PlotSettings('gg') # test reading a bad element bad_elem = QDomElement() self.assertFalse(res.read_xml(bad_elem)) self.assertTrue(res.read_xml(elem)) self.assertEqual(res.plot_type, original.plot_type) self.assertEqual(res.properties, original.properties) self.assertEqual(res.layout, original.layout)
def test_readwrite(self): """ Test reading and writing plot settings from XML """ doc = QDomDocument("properties") original = PlotSettings('test', properties={ 'marker_width': 2, 'marker_size': 5 }, layout={ 'title': 'my plot', 'legend_orientation': 'v' }) original.data_defined_properties.setProperty( PlotSettings.PROPERTY_FILTER, QgsProperty.fromExpression('"ap">50')) original.data_defined_properties.setProperty( PlotSettings.PROPERTY_MARKER_SIZE, QgsProperty.fromExpression('5+6')) original.data_defined_properties.setProperty( PlotSettings.PROPERTY_COLOR, QgsProperty.fromExpression("'red'")) original.data_defined_properties.setProperty( PlotSettings.PROPERTY_STROKE_WIDTH, QgsProperty.fromExpression('12/2')) elem = original.write_xml(doc) self.assertFalse(elem.isNull()) res = PlotSettings('gg') # test reading a bad element bad_elem = QDomElement() self.assertFalse(res.read_xml(bad_elem)) self.assertTrue(res.read_xml(elem)) self.assertEqual(res.plot_type, original.plot_type) self.assertEqual(res.properties, original.properties) self.assertEqual(res.layout, original.layout) self.assertEqual( res.data_defined_properties.property(PlotSettings.PROPERTY_FILTER), original.data_defined_properties.property( PlotSettings.PROPERTY_FILTER)) self.assertEqual( res.data_defined_properties.property( PlotSettings.PROPERTY_MARKER_SIZE), original.data_defined_properties.property( PlotSettings.PROPERTY_MARKER_SIZE)) self.assertEqual( res.data_defined_properties.property(PlotSettings.PROPERTY_COLOR), original.data_defined_properties.property( PlotSettings.PROPERTY_COLOR)) self.assertEqual( res.data_defined_properties.property( PlotSettings.PROPERTY_STROKE_WIDTH), original.data_defined_properties.property( PlotSettings.PROPERTY_STROKE_WIDTH))
class PlotLayoutItem(QgsLayoutItem): def __init__(self, layout): super().__init__(layout) self.setCacheMode(QGraphicsItem.NoCache) self.plot_settings = PlotSettings() self.web_page = LoggingWebPage(self) self.web_page.setNetworkAccessManager( QgsNetworkAccessManager.instance()) # This makes the background transparent. (copied from QgsLayoutItemLabel) palette = self.web_page.palette() palette.setBrush(QPalette.Base, Qt.transparent) self.web_page.setPalette(palette) self.web_page.mainFrame().setZoomFactor(10.0) self.web_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.web_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.web_page.loadFinished.connect(self.loading_html_finished) self.html_loaded = False self.html_units_to_layout_units = self.calculate_html_units_to_layout_units( ) self.sizePositionChanged.connect(self.refresh) def type(self): return ITEM_TYPE def icon(self): return GuiUtils.get_icon('dataplotly.svg') def calculate_html_units_to_layout_units(self): if not self.layout(): return 1 # Hm - why is this? Something internal in Plotly which is auto-scaling the html content? # we may need to expose this as a "scaling" setting return 72 def set_plot_settings(self, settings): """ Sets the plot settings to show in the item """ self.plot_settings = settings self.html_loaded = False self.invalidateCache() def draw(self, context): if not self.html_loaded: self.load_content() # almost a direct copy from QgsLayoutItemLabel! painter = context.renderContext().painter() painter.save() # painter is scaled to dots, so scale back to layout units painter.scale( context.renderContext().scaleFactor() / self.html_units_to_layout_units, context.renderContext().scaleFactor() / self.html_units_to_layout_units) self.web_page.mainFrame().render(painter) painter.restore() def create_plot(self): factory = PlotFactory(self.plot_settings) config = {'displayModeBar': False, 'staticPlot': True} return factory.build_html(config) def load_content(self): self.html_loaded = False base_url = QUrl.fromLocalFile( self.layout().project().absoluteFilePath()) self.web_page.setViewportSize( QSize(self.rect().width() * self.html_units_to_layout_units, self.rect().height() * self.html_units_to_layout_units)) self.web_page.mainFrame().setHtml(self.create_plot(), base_url) def writePropertiesToElement(self, element, document, _): element.appendChild(self.plot_settings.write_xml(document)) return True def readPropertiesFromElement(self, element, document, context): res = self.plot_settings.read_xml(element.firstChildElement('Option')) self.html_loaded = False self.invalidateCache() return res def loading_html_finished(self): self.html_loaded = True self.invalidateCache() self.update() def refresh(self): super().refresh() self.html_loaded = False self.invalidateCache()
class PlotLayoutItem(QgsLayoutItem): def __init__(self, layout): super().__init__(layout) self.setCacheMode(QGraphicsItem.NoCache) self.plot_settings = PlotSettings() self.linked_map_uuid = '' self.linked_map = None self.filter_by_map = False self.filter_by_atlas = False self.web_page = LoggingWebPage(self) self.web_page.setNetworkAccessManager( QgsNetworkAccessManager.instance()) # This makes the background transparent. (copied from QgsLayoutItemLabel) palette = self.web_page.palette() palette.setBrush(QPalette.Base, Qt.transparent) self.web_page.setPalette(palette) self.web_page.mainFrame().setZoomFactor(10.0) self.web_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) self.web_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.web_page.loadFinished.connect(self.loading_html_finished) self.html_loaded = False self.html_units_to_layout_units = self.calculate_html_units_to_layout_units( ) self.sizePositionChanged.connect(self.refresh) def type(self): return ITEM_TYPE def icon(self): return GuiUtils.get_icon('dataplotly.svg') def calculate_html_units_to_layout_units(self): if not self.layout(): return 1 # Hm - why is this? Something internal in Plotly which is auto-scaling the html content? # we may need to expose this as a "scaling" setting return 72 def set_linked_map(self, map): """ Sets the map linked to the plot item """ if self.linked_map == map: return self.disconnect_current_map() self.linked_map = map self.linked_map.extentChanged.connect(self.map_extent_changed) self.linked_map.mapRotationChanged.connect(self.map_extent_changed) self.linked_map.destroyed.connect(self.disconnect_current_map) def disconnect_current_map(self): if not self.linked_map: return try: self.linked_map.extentChanged.disconnect(self.map_extent_changed) self.linked_map.mapRotationChanged.disconnect( self.map_extent_changed) self.linked_map.destroyed.disconnect(self.disconnect_current_map) except RuntimeError: # c++ object already gone! pass self.linked_map = None def set_plot_settings(self, settings): """ Sets the plot settings to show in the item """ self.plot_settings = settings self.html_loaded = False self.invalidateCache() def draw(self, context): if not self.html_loaded: self.load_content() # almost a direct copy from QgsLayoutItemLabel! painter = context.renderContext().painter() painter.save() # painter is scaled to dots, so scale back to layout units painter.scale( context.renderContext().scaleFactor() / self.html_units_to_layout_units, context.renderContext().scaleFactor() / self.html_units_to_layout_units) self.web_page.mainFrame().render(painter) painter.restore() def create_plot(self): if self.linked_map and self.filter_by_map: polygon_filter = FilterRegion( QgsGeometry.fromQPolygonF( self.linked_map.visibleExtentPolygon()), self.linked_map.crs()) self.plot_settings.properties['visible_features_only'] = True elif self.filter_by_atlas and self.layout().reportContext().layer( ) and self.layout().reportContext().feature().isValid(): polygon_filter = FilterRegion( self.layout().reportContext().currentGeometry(), self.layout().reportContext().layer().crs()) self.plot_settings.properties['visible_features_only'] = True else: polygon_filter = None self.plot_settings.properties['visible_features_only'] = False factory = PlotFactory(self.plot_settings, self, polygon_filter=polygon_filter) config = {'displayModeBar': False, 'staticPlot': True} return factory.build_html(config) def load_content(self): self.html_loaded = False base_url = QUrl.fromLocalFile( self.layout().project().absoluteFilePath()) self.web_page.setViewportSize( QSize(self.rect().width() * self.html_units_to_layout_units, self.rect().height() * self.html_units_to_layout_units)) self.web_page.mainFrame().setHtml(self.create_plot(), base_url) def writePropertiesToElement(self, element, document, _): element.appendChild(self.plot_settings.write_xml(document)) element.setAttribute('filter_by_map', 1 if self.filter_by_map else 0) element.setAttribute('filter_by_atlas', 1 if self.filter_by_atlas else 0) element.setAttribute('linked_map', self.linked_map.uuid() if self.linked_map else '') return True def readPropertiesFromElement(self, element, document, context): res = self.plot_settings.read_xml(element.firstChildElement('Option')) self.filter_by_map = bool(int(element.attribute('filter_by_map', '0'))) self.filter_by_atlas = bool( int(element.attribute('filter_by_atlas', '0'))) self.linked_map_uuid = element.attribute('linked_map') self.disconnect_current_map() self.html_loaded = False self.invalidateCache() return res def finalizeRestoreFromXml(self): # has to happen after ALL items have been restored if self.layout() and self.linked_map_uuid: self.disconnect_current_map() map = self.layout().itemByUuid(self.linked_map_uuid) if map: self.set_linked_map(map) def loading_html_finished(self): self.html_loaded = True self.invalidateCache() self.update() def refresh(self): super().refresh() self.html_loaded = False self.invalidateCache() def map_extent_changed(self): if not self.linked_map or not self.filter_by_map: return self.html_loaded = False self.invalidateCache() self.update()