Chart¶
A PyObjC Example without documentation
Sources¶
AppController.py¶
import Cocoa
import objc
import Quartz # noqa: F401
from objc import super # noqa: A004
# APPLICATION DATA STORAGE NOTES:
# - This application uses a simple data storage as an array of entries,
# each containing two attributes: a label and a value
# - The array is represented as a NSMutableArray, the entries as
# NSMutableDictionaries, the label as a NSString with the "label" key and
# the value as a NSNumber with the "value" key
#
#
# QUARTZ COMPOSER COMPOSITION NOTES:
# - The enclosed Quartz Composer composition renders a 3D bars chart and is
# loaded on the QCView in the application's window
# - This composition has three input parameters:
# * "Data": the data to display by the chart which must be formatted as a
# NSArray of NSDictionaries, each NSDictionary containing "label" / NSString
# and "value" / NSNumber value-key pairs
# * "Scale": a NSNumber used to scale the chart bars
# * "Spacing": a NSNumber indicating the extra spacing between the chart bars
# - The "Data" and "Scale" input parameters are set programmatically while the
# "Spacing" is set directly from the UI through Cocoa bindings
# - Note that this composition is quite simple and has the following
# limitations:
# * it may have rendering artifacts when looking at the chart from some angles
# * it does not support negative values
# * labels are not truncated if too long
# - Basically, the composition performs the following:
# * renders a background gradient
# * draws three planes on the X, Y and Z axes
# * uses an Iterator patch to loop on the chart data, which is available as
# a Structure, and for each member, retrieves the label and value, then
# draws them
# * the chart rendering is enclosed into a Camera macro patch used to center
# it in the view
# * the Camera macro patch is itself enclosed into a TrackBall macro patch
# so that the user can rotate the chart with the mouse
# * the TrackBall macro patch is itself enclosed into a Lighting macro patch
# so that the chart is lighted
# - This composition makes uses of transparency for a nicer effect, but
# neither OpenGL nor Quartz Composer handle automatically proper rendering
# of mixed opaque and transparent 3D objects
# - A simple, but not fail-proof, algorithm to render opaque and transparent
# 3D objects is to:
# * render opaque objects first with depth testing set to "Read / Write"
# * render transparent objects with depth testing set to "Read-Only"
#
#
# NIB FILES NOTES:
# - The QCView is configured to start rendering automatically and forward user
# events (mouse events are required to rotate the chart)
# - An AppController instance is connected as the data source for the
# NSTableView
# - The NSTableView is set up so that the identifiers of table columns match
# the keys used in the data storage
# - The "Value" column of the NSTableView has a NSNumberFormatter which
# guarantees only positive or null numbers can be entered here
# - The "Label" column of the NSTableView simply contains text
#
# Keys for the entries in the data storage
kDataKey_Label = "label" # NSString
kDataKey_Value = "value" # NSNumber
# Keys for the composition input parameters
kParameterKey_Data = "Data" # NSArray of NSDictionaries
kParameterKey_Scale = "Scale" # NSNumber
kParameterKey_Spacing = "Spacing" # NSNumber
class AppController(Cocoa.NSObject):
tableView = objc.IBOutlet()
view = objc.IBOutlet()
_data = objc.ivar()
def init(self):
# Allocate our data storage
self = super().init()
if self is None:
return None
self._data = []
return self
def awakeFromNib(self):
# Load the composition file into the QCView (because this
# QCView is bound to a QCPatchController in the nib file, this
# will actually update the QCPatchController along with all the
# bindings)
if not self.view.loadCompositionFromFile_(
Cocoa.NSBundle.mainBundle().pathForResource_ofType_("Chart", "qtz")
):
Cocoa.NSLog("Composition loading failed")
Cocoa.NSApp.terminate_(None)
# Populate data storage
self._data.extend(
[
{kDataKey_Label: "Palo Alto", kDataKey_Value: 2},
{kDataKey_Label: "Cupertino", kDataKey_Value: 1},
{kDataKey_Label: "Menlo Park", kDataKey_Value: 4},
{kDataKey_Label: "Mountain View", kDataKey_Value: 8},
{kDataKey_Label: "San Francisco", kDataKey_Value: 7},
{kDataKey_Label: "Los Altos", kDataKey_Value: 3},
]
)
# Initialize the views
self.tableView.reloadData()
self.updateChart()
def updateChart(self):
# Update the data displayed by the chart - it will be converted to a
# Structure of Structures by Quartz Composer
self.view.setValue_forInputKey_(self._data, kParameterKey_Data)
# Compute the maximum value and set the chart scale accordingly
max_value = 0.0
for obj in self._data:
value = obj[kDataKey_Value]
if value > max_value:
max_value = value
if max_value == 0.0:
scale = 1.0
else:
scale = 1 / max_value
self.view.setValue_forInputKey_(scale, kParameterKey_Scale)
@objc.IBAction
def addEntry_(self, sender):
# Add a new entry to the data storage
self._data.append({kDataKey_Label: "Untitled", kDataKey_Value: 0})
# Notify the NSTableView and update the chart
self.tableView.reloadData()
self.updateChart()
# Automatically select and edit the new entry
self.tableView.selectRow_byExtendingSelection_(len(self._data) - 1, False)
self.tableView.editColumn_row_withEvent_select_(
self.tableView.columnWithIdentifier_(kDataKey_Label),
len(self._data) - 1,
None,
True,
)
@objc.IBAction
def removeEntry_(self, sender):
# Make sure we have a valid selected row
selectedRow = self.tableView.selectedRow()
if selectedRow < 0 or self.tableView.editedRow() == selectedRow:
return
# Remove the currently selected entry from the data storage
del self._data[selectedRow]
# Notify the NSTableView and update the chart
self.tableView.reloadData()
self.updateChart()
def numberOfRowsInTableView_(self, aTableView):
# Return the number of entries in the data storage
return len(self._data)
def tableView_objectValueForTableColumn_row_(
self, aTableView, aTableColumn, rowIndex
):
# Get the "label" or "value" attribute of the entry from the data
# storage at index "rowIndex"
return self._data[rowIndex][aTableColumn.identifier()]
def tableView_setObjectValue_forTableColumn_row_(
self, aTableView, anObject, aTableColumn, rowIndex
):
# Set the "label" or "value" attribute of the entry from the data
# storage at index "rowIndex"
self._data[rowIndex][aTableColumn.identifier()] = anObject
# Update the chart
self.updateChart()
main.py¶
import AppController # noqa: F401
from PyObjCTools import AppHelper
AppHelper.runEventLoop()
setup.py¶
"""
Script for building the example.
Usage:
python3 setup.py py2app
"""
from setuptools import setup
setup(
name="PyChart",
app=["main.py"],
data_files=["English.lproj", "Chart.qtz"],
setup_requires=["py2app", "pyobjc-framework-Cocoa", "pyobjc-framework-Quartz"],
)