# Chart

- `Download example`

A simple application that displays a barchart using Quartz Composer.

## Sources

### AppController.py

    import Cocoa
    import objc
    import Quartz  # noqa: F401
    from objc import super

    # 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(AppController, self).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"],
    )