Exemple #1
0
    def draw(self, dataTable, functionTable, performanceTable, rowIndex,
             colIndex, cellContents, labelAttributes, plotDefinitions):
        """Draw the plot legend content, which is more often text than graphics.

        @type dataTable: DataTable
        @param dataTable: Contains the data to describe, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type rowIndex: int
        @param rowIndex: Row number of the C{cellContents} to fill.
        @type colIndex: int
        @param colIndex: Column number of the C{cellContents} to fill.
        @type cellContents: dict
        @param cellContents: Dictionary that maps pairs of integers to SVG graphics to draw.
        @type labelAttributes: CSS style dict
        @param labelAttributes: Style properties that are defined at the level of the legend and must percolate down to all drawables within the legend.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: 2-tuple
        @return: The next C{rowIndex} and C{colIndex} in the sequence.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLegendNumber")

        myLabelAttributes = dict(labelAttributes)
        style = PlotStyle.toDict(myLabelAttributes["style"])
        style.update(self.getStyleState())
        myLabelAttributes["style"] = PlotStyle.toString(style)
        myLabelAttributes["font-size"] = style["font-size"]

        svgId = self.get("svgId")
        if svgId is not None:
            myLabelAttributes["id"] = svgId

        try:
            float(self.text)
        except (ValueError, TypeError):
            self.text = "0"

        digits = self.get("digits")
        if digits is not None:
            astext = PlotNumberFormat.roundDigits(float(self.text),
                                                  int(digits))
        else:
            astext = PlotNumberFormat.toUnicode(self.text)

        cellContents[rowIndex, colIndex] = svg.text(astext,
                                                    **myLabelAttributes)
        colIndex += 1

        performanceTable.end("PlotLegendNumber")
        return rowIndex, colIndex
Exemple #2
0
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotCurve draw")

        loop = self.get("loop", defaultFromXsd=True, convertType=True)        
        pathdata = self.formatPathdata(state.x, state.y, state.dx, state.dy, plotCoordinates, loop, (state.dx is not None and state.dy is not None))
        output = svg.g()

        style = self.getStyleState()
        strokeStyle = dict((x, style[x]) for x in style if x.startswith("stroke"))
        fillStyle = dict((x, style[x]) for x in style if x.startswith("fill"))
        fillStyle["stroke"] = "none"

        if style["fill"] != "none":
            if len(self.xpath("pmml:PlotFormula[@role='y(x)']")) > 0 and len(pathdata) > 1:
                firstPoint = plotCoordinates(state.x[0], 0.0)
                lastPoint = plotCoordinates(state.x[-1], 0.0)

                X0, Y0 = plotCoordinates(state.x[0], state.y[0])

                pathdata2 = ["M %r %r" % firstPoint]
                pathdata2.append("L %r %r" % (X0, Y0))
                pathdata2.extend(pathdata[1:])
                pathdata2.append("L %r %r" % lastPoint)

                output.append(svg.path(d=" ".join(pathdata2), style=PlotStyle.toString(fillStyle)))

            else:
                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(fillStyle)))

        output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(strokeStyle)))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotCurve draw")
        return output
    def draw(self, dataTable, functionTable, performanceTable, rowIndex, colIndex, cellContents, labelAttributes, plotDefinitions):
        """Draw the plot legend content, which is more often text than graphics.

        @type dataTable: DataTable
        @param dataTable: Contains the data to describe, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type rowIndex: int
        @param rowIndex: Row number of the C{cellContents} to fill.
        @type colIndex: int
        @param colIndex: Column number of the C{cellContents} to fill.
        @type cellContents: dict
        @param cellContents: Dictionary that maps pairs of integers to SVG graphics to draw.
        @type labelAttributes: CSS style dict
        @param labelAttributes: Style properties that are defined at the level of the legend and must percolate down to all drawables within the legend.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: 2-tuple
        @return: The next C{rowIndex} and C{colIndex} in the sequence.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLegendNumber")

        myLabelAttributes = dict(labelAttributes)
        style = PlotStyle.toDict(myLabelAttributes["style"])
        style.update(self.getStyleState())
        myLabelAttributes["style"] = PlotStyle.toString(style)
        myLabelAttributes["font-size"] = style["font-size"]

        svgId = self.get("svgId")
        if svgId is not None:
            myLabelAttributes["id"] = svgId

        try:
            float(self.text)
        except (ValueError, TypeError):
            self.text = "0"

        digits = self.get("digits")
        if digits is not None:
            astext = PlotNumberFormat.roundDigits(float(self.text), int(digits))
        else:
            astext = PlotNumberFormat.toUnicode(self.text)

        cellContents[rowIndex, colIndex] = svg.text(astext, **myLabelAttributes)
        colIndex += 1

        performanceTable.end("PlotLegendNumber")
        return rowIndex, colIndex
    def drawErrorbars(xarray, yarray, exup, exdown, eyup, eydown, markerSize, strokeStyle, weight=None):
        """Draw a set of error bars, given values in global SVG
        coordinates.

        @type xarray: 1d Numpy array
        @param xarray: The X positions in global SVG coordinates.
        @type yarray: 1d Numpy array
        @param yarray: The Y positions in global SVG coordinates.
        @type exup: 1d Numpy array or None
        @param exup: The upper ends of the X error bars in global SVG coordinates (already added to the X positions).
        @type exdown: 1d Numpy array or None
        @param exdown: The lower ends of the X error bars in global SVG coordinates (already added to the X positions).
        @type eyup: 1d Numpy array or None
        @param eyup: The upper ends of the Y error bars in global SVG coordinates (already added to the Y positions).
        @type eydown: 1d Numpy array or None
        @param eydown: The lower ends of the Y error bars in global SVG coordinates (already added to the Y positions).
        @type markerSize: number
        @param markerSize: Size of the marker in SVG coordinates.
        @type strokeStyle: dict
        @param strokeStyle: CSS style attributes appropriate for stroking (not filling) in dictionary form.
        @type weight: 1d Numpy array or None
        @param weight: The opacity of each point (if None, the opacity is not specified and is therefore fully opaque).
        """

        svg = SvgBinding.elementMaker
        output = []
        
        strokeStyle = copy.copy(strokeStyle)
        strokeStyle["fill"] = "none"
        if weight is not None:
            strokeStyle["opacity"] = "1"

        for i in xrange(len(xarray)):
            x = xarray[i]
            y = yarray[i]

            pathdata = []

            if exup is not None:
                pathdata.append("M %r %r L %r %r" % (exdown[i], y             ,   exup[i], y             ))
                pathdata.append("M %r %r L %r %r" % (exdown[i], y - markerSize, exdown[i], y + markerSize))
                pathdata.append("M %r %r L %r %r" % (  exup[i], y - markerSize,   exup[i], y + markerSize))

            if eyup is not None:
                pathdata.append("M %r %r L %r %r" % (x             , eydown[i], x             ,   eyup[i]))
                pathdata.append("M %r %r L %r %r" % (x - markerSize, eydown[i], x + markerSize, eydown[i]))
                pathdata.append("M %r %r L %r %r" % (x - markerSize, eyup[i],   x + markerSize,   eyup[i]))

            if len(pathdata) > 0:
                if weight is not None:
                    strokeStyle["opacity"] = repr(weight[i])
                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(strokeStyle)))

        return output
Exemple #5
0
    def drawErrorbars(xarray, yarray, exup, exdown, eyup, eydown, markerSize, strokeStyle, weight=None):
        """Draw a set of error bars, given values in global SVG
        coordinates.

        @type xarray: 1d Numpy array
        @param xarray: The X positions in global SVG coordinates.
        @type yarray: 1d Numpy array
        @param yarray: The Y positions in global SVG coordinates.
        @type exup: 1d Numpy array or None
        @param exup: The upper ends of the X error bars in global SVG coordinates (already added to the X positions).
        @type exdown: 1d Numpy array or None
        @param exdown: The lower ends of the X error bars in global SVG coordinates (already added to the X positions).
        @type eyup: 1d Numpy array or None
        @param eyup: The upper ends of the Y error bars in global SVG coordinates (already added to the Y positions).
        @type eydown: 1d Numpy array or None
        @param eydown: The lower ends of the Y error bars in global SVG coordinates (already added to the Y positions).
        @type markerSize: number
        @param markerSize: Size of the marker in SVG coordinates.
        @type strokeStyle: dict
        @param strokeStyle: CSS style attributes appropriate for stroking (not filling) in dictionary form.
        @type weight: 1d Numpy array or None
        @param weight: The opacity of each point (if None, the opacity is not specified and is therefore fully opaque).
        """

        svg = SvgBinding.elementMaker
        output = []
        
        strokeStyle = copy.copy(strokeStyle)
        strokeStyle["fill"] = "none"
        if weight is not None:
            strokeStyle["opacity"] = "1"

        for i in xrange(len(xarray)):
            x = xarray[i]
            y = yarray[i]

            pathdata = []

            if exup is not None:
                pathdata.append("M %r %r L %r %r" % (exdown[i], y             ,   exup[i], y             ))
                pathdata.append("M %r %r L %r %r" % (exdown[i], y - markerSize, exdown[i], y + markerSize))
                pathdata.append("M %r %r L %r %r" % (  exup[i], y - markerSize,   exup[i], y + markerSize))

            if eyup is not None:
                pathdata.append("M %r %r L %r %r" % (x             , eydown[i], x             ,   eyup[i]))
                pathdata.append("M %r %r L %r %r" % (x - markerSize, eydown[i], x + markerSize, eydown[i]))
                pathdata.append("M %r %r L %r %r" % (x - markerSize, eyup[i],   x + markerSize,   eyup[i]))

            if len(pathdata) > 0:
                if weight is not None:
                    strokeStyle["opacity"] = repr(weight[i])
                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(strokeStyle)))

        return output
Exemple #6
0
    def checkStyleProperties(self):
        """Verify that all properties currently requested in the
        C{style} attribute are in the legal C{styleProperties} list.

        @raise PmmlValidationError: If the list contains an unrecognized style property name, raise an error.  Otherwise, silently pass.
        """

        style = self.get("style")
        if style is not None:
            for name in PlotStyle.toDict(style).keys():
                if name not in self.styleProperties:
                    raise defs.PmmlValidationError("Unrecognized style property: \"%s\"" % name)
Exemple #7
0
    def getStyleState(self):
        """Get the current state of the style (including any
        unmodified defaults) as a dictionary.

        @rtype: dict
        @return: Dictionary mapping style property names to their values (as strings).
        """

        style = dict(self.styleDefaults)
        currentStyle = self.get("style")
        if currentStyle is not None:
            style.update(PlotStyle.toDict(currentStyle))
        return style
Exemple #8
0
    def checkStyleProperties(self):
        """Verify that all properties currently requested in the
        C{style} attribute are in the legal C{styleProperties} list.

        @raise PmmlValidationError: If the list contains an unrecognized style property name, raise an error.  Otherwise, silently pass.
        """

        style = self.get("style")
        if style is not None:
            for name in PlotStyle.toDict(style).keys():
                if name not in self.styleProperties:
                    raise defs.PmmlValidationError(
                        "Unrecognized style property: \"%s\"" % name)
Exemple #9
0
    def getStyleState(self):
        """Get the current state of the style (including any
        unmodified defaults) as a dictionary.

        @rtype: dict
        @return: Dictionary mapping style property names to their values (as strings).
        """

        style = dict(self.styleDefaults)
        currentStyle = self.get("style")
        if currentStyle is not None:
            style.update(PlotStyle.toDict(currentStyle))
        return style
Exemple #10
0
    def draw(self, dataTable, functionTable, performanceTable, plotCoordinates,
             plotContentBox, plotDefinitions):
        """Draw the plot annotation.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed.
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker

        svgId = self.get("svgId")
        if svgId is None:
            output = svg.g()
        else:
            output = svg.g(**{"id": svgId})
        content = [output]

        inlineSvg = self.getchildren()
        fileName = self.get("fileName")
        if len(inlineSvg) == 1 and fileName is None:
            svgBinding = inlineSvg[0]
        elif len(inlineSvg) == 0 and fileName is not None:
            svgBinding = SvgBinding.loadXml(fileName)
        else:
            raise defs.PmmlValidationError(
                "PlotSvgAnnotation should specify an inline SVG or a fileName but not both or neither"
            )

        style = self.getStyleState()

        if style.get("margin-bottom") == "auto": del style["margin-bottom"]
        if style.get("margin-top") == "auto": del style["margin-top"]
        if style.get("margin-left") == "auto": del style["margin-left"]
        if style.get("margin-right") == "auto": del style["margin-right"]

        subContentBox = plotContentBox.subContent(style)
        sx1, sy1, sx2, sy2 = PlotSvgAnnotation.findSize(svgBinding)
        nominalHeight = sy2 - sy1
        nominalWidth = sx2 - sx1

        if nominalHeight < subContentBox.height:
            if "margin-bottom" in style and "margin-top" in style:
                pass
            elif "margin-bottom" in style:
                style["margin-top"] = subContentBox.height - nominalHeight
            elif "margin-top" in style:
                style["margin-bottom"] = subContentBox.height - nominalHeight
            else:
                style["margin-bottom"] = style["margin-top"] = (
                    subContentBox.height - nominalHeight) / 2.0

        if nominalWidth < subContentBox.width:
            if "margin-left" in style and "margin-right" in style:
                pass
            elif "margin-left" in style:
                style["margin-right"] = subContentBox.width - nominalWidth
            elif "margin-right" in style:
                style["margin-left"] = subContentBox.width - nominalWidth
            else:
                style["margin-left"] = style["margin-right"] = (
                    subContentBox.width - nominalWidth) / 2.0

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        if subContentBox is not None:
            tx1, ty1 = plotCoordinates(subContentBox.x, subContentBox.y)
            tx2, ty2 = plotCoordinates(subContentBox.x + subContentBox.width,
                                       subContentBox.y + subContentBox.height)

            output.extend([copy.deepcopy(x) for x in svgBinding.getchildren()])

            output["transform"] = "translate(%r, %r) scale(%r, %r)" % (
                tx1 - sx1, ty1 - sy1, (tx2 - tx1) / float(sx2 - sx1),
                (ty2 - ty1) / float(sy2 - sy1))

        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace(
                            "border-", "stroke-")] = style[styleProperty]

                x1 = borderRect.x
                y1 = borderRect.y
                x2 = borderRect.x + borderRect.width
                y2 = borderRect.y + borderRect.height
                x1, y1 = plotCoordinates(x1, y1)
                x2, y2 = plotCoordinates(x2, y2)

                subAttrib = {
                    "x": repr(x1),
                    "y": repr(y1),
                    "width": repr(x2 - x1),
                    "height": repr(y2 - y1),
                    "style": PlotStyle.toString(rectStyle)
                }

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        return svg.g(*content)
class PlotBoxAndWhisker(PmmlPlotContent):
    """Represents a "box-and-whiskers" plot or a "profile histogram."

    PMML subelements:

      - PlotExpression role="sliced": expression to be sliced like a
        histogram.
      - PlotNumericExpression role="profiled": expression to be
        profiled in each slice.
      - PlotSelection: expression or predicate to filter the data
        before plotting.
      - Intervals: non-uniform (numerical) histogram bins.
      - Values: explicit (categorical) histogram values.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - stateId: key for persistent storage in a DataTableState.
      - numBins: number of histogram bins.
      - low: histogram low edge.
      - high: histogram high edge.
      - levels: "percentage" for quartile-like box-and-whiskers,
        "standardDeviation" for mean and standard deviation, as in
        a profile histogram.
      - lowWhisker: bottom of the lower whisker, usually the 0th
        percentile (absolute minimum).
      - lowBox: bottom of the box, usually the 25th percentile.
      - midLine: middle line of the box, usually the median.
      - highBox: top of the box, usually the 75th percentile.
      - highWhisker: top of the upper whisker, usually the 100th
        percentile (absolute maximum).
      - vertical: if "true", plot the "sliced" expression on the
        x axis and the "profiled" expression on the y axis.
      - gap: size of the space between boxes in SVG coordinates.
      - style: CSS style properties.

    CSS properties:

      - fill, fill-opacity: color of the box.
      - stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap,
        stroke-linejoin, stroke-miterlimit, stroke-opacity,
        stroke-width: properties of the line drawing the box and
        the whiskers.

    See the source code for the full XSD.
    """

    styleProperties = ["fill", "fill-opacity", 
                       "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width",
                       ]

    styleDefaults = {"fill": "none", "stroke": "black"}

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotBoxAndWhisker">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:element ref="PlotExpression" minOccurs="1" maxOccurs="1" />
                <xs:element ref="PlotNumericExpression" minOccurs="1" maxOccurs="1" />
                <xs:element ref="PlotSelection" minOccurs="0" maxOccurs="1" />
                <xs:choice minOccurs="0" maxOccurs="1">
                    <xs:element ref="Interval" minOccurs="1" maxOccurs="unbounded" />
                    <xs:element ref="Value" minOccurs="1" maxOccurs="unbounded" />
                </xs:choice>
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="stateId" type="xs:string" use="optional" />
            <xs:attribute name="numBins" type="xs:positiveInteger" use="optional" />
            <xs:attribute name="low" type="xs:double" use="optional" />
            <xs:attribute name="high" type="xs:double" use="optional" />
            <xs:attribute name="levels" use="optional" default="percentage">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                        <xs:enumeration value="percentage" />
                        <xs:enumeration value="standardDeviation" />
                    </xs:restriction>
                </xs:simpleType>
            </xs:attribute>
            <xs:attribute name="lowWhisker" type="xs:double" use="optional" default="0" />
            <xs:attribute name="lowBox" type="xs:double" use="optional" default="25" />
            <xs:attribute name="midLine" type="xs:double" use="optional" default="50" />
            <xs:attribute name="highBox" type="xs:double" use="optional" default="75" />
            <xs:attribute name="highWhisker" type="xs:double" use="optional" default="100" />
            <xs:attribute name="vertical" type="xs:boolean" use="optional" default="true" />
            <xs:attribute name="gap" type="xs:double" use="optional" default="10" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    fieldTypeNumeric = FakeFieldType("double", "continuous")

    def prepare(self, state, dataTable, functionTable, performanceTable, plotRange):
        """Prepare a plot element for drawing.

        This stage consists of calculating all quantities and
        determing the bounds of the data.  These bounds may be unioned
        with bounds from other plot elements that overlay this plot
        element, so the drawing (which requires a finalized coordinate
        system) cannot begin yet.

        This method modifies C{plotRange}.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotRange: PlotRange
        @param plotRange: The bounding box of plot coordinates that this function will expand.
        """

        self.checkRoles(["sliced", "profiled"])

        slicedExpression = self.xpath("pmml:PlotExpression[@role='sliced']")
        profiledExpression = self.xpath("pmml:PlotNumericExpression[@role='profiled']")
        cutExpression = self.xpath("pmml:PlotSelection")
        if len(slicedExpression) != 1:
            raise defs.PmmlValidationError("PlotHistogram requires a PlotExpression with role \"sliced\"")
        if len(profiledExpression) != 1:
            raise defs.PmmlValidationError("PlotHistogram requires a PlotNumericExpression with role \"profiled\"")

        slicedDataColumn = slicedExpression[0].evaluate(dataTable, functionTable, performanceTable)
        profiledDataColumn = profiledExpression[0].evaluate(dataTable, functionTable, performanceTable)

        if len(cutExpression) == 1:
            selection = cutExpression[0].select(dataTable, functionTable, performanceTable)
        else:
            selection = NP("ones", len(dataTable), NP.dtype(bool))

        performanceTable.begin("PlotBoxAndWhisker prepare")
        self._saveContext(dataTable)

        if slicedDataColumn.mask is not None:
            NP("logical_and", selection, NP(slicedDataColumn.mask == defs.VALID), selection)
        if profiledDataColumn.mask is not None:
            NP("logical_and", selection, NP(profiledDataColumn.mask == defs.VALID), selection)

        slicedArray = slicedDataColumn.data[selection]
        profiledArray = profiledDataColumn.data[selection]
        
        persistentState = {}
        stateId = self.get("stateId")
        if stateId is not None:
            if stateId in dataTable.state:
                persistentState = dataTable.state[stateId]
            else:
                dataTable.state[stateId] = persistentState

        intervals = self.xpath("pmml:Interval")
        values = self.xpath("pmml:Value")

        if "binType" not in persistentState:
            performanceTable.begin("establish binType")

            binType = PlotHistogram.establishBinType(slicedDataColumn.fieldType, intervals, values)
            persistentState["binType"] = binType

            if binType == "nonuniform":
                persistentState["distributions"] = [NP("empty", 0, dtype=profiledDataColumn.fieldType.dtype) for x in xrange(len(intervals))]

            elif binType == "explicit":
                persistentState["distributions"] = [NP("empty", 0, dtype=profiledDataColumn.fieldType.dtype) for x in xrange(len(values))]

            elif binType == "unique":
                persistentState["distributions"] = {}

            elif binType == "scale":
                numBins = self.get("numBins", convertType=True)
                low = self.get("low", convertType=True)
                high = self.get("high", convertType=True)

                numBins, low, high = PlotHistogram.determineScaleBins(numBins, low, high, slicedArray)

                persistentState["low"] = low
                persistentState["high"] = high
                persistentState["numBins"] = numBins
                persistentState["distributions"] = [NP("empty", 0, dtype=profiledDataColumn.fieldType.dtype) for x in xrange(numBins)]

            performanceTable.end("establish binType")

        if persistentState["binType"] == "nonuniform":
            performanceTable.begin("binType nonuniform")

            distributions = [None] * len(intervals)
            state.edges = []
            lastLimitPoint = None
            lastClosed = None
            lastInterval = None

            for index, interval in enumerate(intervals):
                selection, lastLimitPoint, lastClosed, lastInterval = PlotHistogram.selectInterval(slicedDataColumn.fieldType, slicedArray, index, len(intervals) - 1, interval, state.edges, lastLimitPoint, lastClosed, lastInterval)

                if selection is None:
                    distributions[index] = profiledArray
                else:
                    distributions[index] = profiledArray[selection]

            persistentState["distributions"] = [NP("concatenate", [x, y]) for x, y in itertools.izip(persistentState["distributions"], distributions)]
            distributions = persistentState["distributions"]
            lowEdge = min(low for low, high in state.edges if low is not None)
            highEdge = max(high for low, high in state.edges if high is not None)
            state.slicedFieldType = self.fieldTypeNumeric

            performanceTable.end("binType nonuniform")

        elif persistentState["binType"] == "explicit":
            performanceTable.begin("binType explicit")

            distributions = [None] * len(values)
            displayValues = []

            for index, value in enumerate(values):
                internalValue = slicedDataColumn.fieldType.stringToValue(value["value"])
                displayValues.append(value.get("displayValue", slicedDataColumn.fieldType.valueToString(internalValue, displayValue=True)))

                selection = NP(slicedArray == internalValue)
                distributions[index] = profiledArray[selection]
                
            persistentState["distributions"] = [NP("concatenate", [x, y]) for x, y in itertools.izip(persistentState["distributions"], distributions)]
            distributions = persistentState["distributions"]
            state.edges = displayValues
            state.slicedFieldType = slicedDataColumn.fieldType

            performanceTable.end("binType explicit")

        elif persistentState["binType"] == "unique":
            performanceTable.begin("binType unique")

            uniques, inverse = NP("unique", slicedArray, return_inverse=True)

            persistentDistributions = persistentState["distributions"]
            for i, u in enumerate(uniques):
                string = slicedDataColumn.fieldType.valueToString(u, displayValue=False)
                selection = NP(inverse == i)

                if string in persistentDistributions:
                    persistentDistributions[string] = NP("concatenate", [persistentDistributions[string], profiledArray[selection]])
                else:
                    persistentDistributions[string] = profiledArray[selection]

            tosort = [(len(distribution), string) for string, distribution in persistentDistributions.items()]
            tosort.sort(reverse=True)

            numBins = self.get("numBins", convertType=True)
            if numBins is not None:
                tosort = tosort[:numBins]

            distributions = [persistentDistributions[string] for count, string in tosort]
            state.edges = [slicedDataColumn.fieldType.valueToString(slicedDataColumn.fieldType.stringToValue(string), displayValue=True) for count, string in tosort]
            state.slicedFieldType = slicedDataColumn.fieldType
            
            performanceTable.end("binType unique")

        elif persistentState["binType"] == "scale":
            performanceTable.begin("binType scale")

            numBins = persistentState["numBins"]
            low = persistentState["low"]
            high = persistentState["high"]
            binWidth = (high - low) / float(numBins)

            binAssignments = NP("array", NP("floor", NP(NP(slicedArray - low)/binWidth)), dtype=NP.dtype(int))
            distributions = [None] * numBins

            for index in xrange(numBins):
                selection = NP(binAssignments == index)
                distributions[index] = profiledArray[selection]
                
            persistentState["distributions"] = [NP("concatenate", [x, y]) for x, y in itertools.izip(persistentState["distributions"], distributions)]
            distributions = persistentState["distributions"]
            state.edges = [(low + i*binWidth, low + (i + 1)*binWidth) for i in xrange(numBins)]
            lowEdge = low
            highEdge = high
            state.slicedFieldType = self.fieldTypeNumeric
        
            performanceTable.end("binType scale")

        levels = self.get("levels", defaultFromXsd=True)
        lowWhisker = self.get("lowWhisker", defaultFromXsd=True, convertType=True)
        lowBox = self.get("lowBox", defaultFromXsd=True, convertType=True)
        midLine = self.get("midLine", defaultFromXsd=True, convertType=True)
        highBox = self.get("highBox", defaultFromXsd=True, convertType=True)
        highWhisker = self.get("highWhisker", defaultFromXsd=True, convertType=True)

        state.ranges = []
        minProfiled = None
        maxProfiled = None
        for distribution in distributions:
            if levels == "percentage":
                if len(distribution) > 0:
                    state.ranges.append(NP("percentile", distribution, [lowWhisker, lowBox, midLine, highBox, highWhisker]))
                else:
                    state.ranges.append(None)

            elif levels == "standardDeviation":
                mu = NP("mean", distribution)
                sigma = NP("std", distribution, ddof=1)

                if NP("isfinite", sigma) and sigma > 0.0:
                    state.ranges.append([(lowWhisker - mu)/sigma, (lowBox - mu)/sigma, (midLine - mu)/sigma, (highBox - mu)/sigma, (highWhisker - mu)/sigma])
                else:
                    state.ranges.append(None)

            if state.ranges[-1] is not None:
                if minProfiled is None:
                    minProfiled = min(state.ranges[-1])
                    maxProfiled = max(state.ranges[-1])
                else:
                    minProfiled = min(minProfiled, min(state.ranges[-1]))
                    maxProfiled = max(maxProfiled, max(state.ranges[-1]))

        state.profiledFieldType = profiledDataColumn.fieldType

        if self.get("vertical", defaultFromXsd=True, convertType=True):
            if state.slicedFieldType is self.fieldTypeNumeric:
                plotRange.xminPush(lowEdge, state.slicedFieldType, sticky=False)
                plotRange.xmaxPush(highEdge, state.slicedFieldType, sticky=False)
                if minProfiled is not None:
                    plotRange.yminPush(minProfiled, state.profiledFieldType, sticky=False)
                    plotRange.ymaxPush(maxProfiled, state.profiledFieldType, sticky=False)

            else:
                strings = NP("array", state.edges, dtype=NP.dtype(object))
                if minProfiled is not None:
                    values = NP("ones", len(state.edges), dtype=state.profiledFieldType.dtype) * maxProfiled
                    values[0] = minProfiled
                else:
                    values = NP("zeros", len(state.edges), dtype=state.profiledFieldType.dtype)

                plotRange.expand(strings, values, state.slicedFieldType, state.profiledFieldType)

        else:
            if state.slicedFieldType is self.fieldTypeNumeric:
                plotRange.yminPush(lowEdge, state.slicedFieldType, sticky=False)
                plotRange.ymaxPush(highEdge, state.slicedFieldType, sticky=False)
                if minProfiled is not None:
                    plotRange.xminPush(minProfiled, state.profiledFieldType, sticky=False)
                    plotRange.xmaxPush(maxProfiled, state.profiledFieldType, sticky=False)

            else:
                strings = NP("array", state.edges, dtype=NP.dtype(object))
                if minProfiled is not None:
                    values = NP("ones", len(state.edges), dtype=state.profiledFieldType.dtype) * maxProfiled
                    values[0] = minProfiled
                else:
                    values = NP("zeros", len(state.edges), dtype=state.profiledFieldType.dtype)
                
                plotRange.expand(values, strings, state.profiledFieldType, state.slicedFieldType)

        performanceTable.end("PlotBoxAndWhisker prepare")

    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotBoxAndWhisker draw")

        vertical = self.get("vertical", defaultFromXsd=True, convertType=True)
        gap = self.get("gap", defaultFromXsd=True, convertType=True)

        if state.slicedFieldType is not self.fieldTypeNumeric:
            if vertical:
                strings = plotCoordinates.xstrings
            else:
                strings = plotCoordinates.ystrings

            newRanges = []
            for string in strings:
                try:
                    index = state.edges.index(string)
                except ValueError:
                    newRanges.append(None)
                else:
                    newRanges.append(state.ranges[index])

            state.ranges = newRanges
            state.edges = [(i - 0.5, i + 0.5) for i in xrange(len(strings))]

        lowEdge = NP("array", [low if low is not None else float("-inf") for low, high in state.edges], dtype=NP.dtype(float))
        highEdge = NP("array", [high if high is not None else float("inf") for low, high in state.edges], dtype=NP.dtype(float))

        selection = NP("array", [levels is not None for levels in state.ranges], dtype=NP.dtype(bool))
        lowEdge = lowEdge[selection]
        highEdge = highEdge[selection]

        lowWhisker  = NP("array", [levels[0] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        lowBox      = NP("array", [levels[1] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        midLine     = NP("array", [levels[2] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        highBox     = NP("array", [levels[3] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        highWhisker = NP("array", [levels[4] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        
        output = svg.g()
        if len(lowEdge) > 0:
            if vertical:
                Ax = lowEdge
                Bx = lowEdge
                Cx = lowEdge
                Dx = highEdge
                Ex = highEdge
                Fx = highEdge
                Gx = NP(NP(lowEdge + highEdge) / 2.0)
                Hx = Gx
                Ix = Gx
                Jx = Gx

                Ay = lowBox
                By = midLine
                Cy = highBox
                Dy = lowBox
                Ey = midLine
                Fy = highBox
                Gy = lowWhisker
                Hy = lowBox
                Iy = highBox
                Jy = highWhisker

            else:
                Ax = lowBox
                Bx = midLine
                Cx = highBox
                Dx = lowBox
                Ex = midLine
                Fx = highBox
                Gx = lowWhisker
                Hx = lowBox
                Ix = highBox
                Jx = highWhisker

                Ay = lowEdge
                By = lowEdge
                Cy = lowEdge
                Dy = highEdge
                Ey = highEdge
                Fy = highEdge
                Gy = NP(NP(lowEdge + highEdge) / 2.0)
                Hy = Gy
                Iy = Gy
                Jy = Gy

            AX, AY = plotCoordinates(Ax, Ay)
            BX, BY = plotCoordinates(Bx, By)
            CX, CY = plotCoordinates(Cx, Cy)
            DX, DY = plotCoordinates(Dx, Dy)
            EX, EY = plotCoordinates(Ex, Ey)
            FX, FY = plotCoordinates(Fx, Fy)
            GX, GY = plotCoordinates(Gx, Gy)
            HX, HY = plotCoordinates(Hx, Hy)
            IX, IY = plotCoordinates(Ix, Iy)
            JX, JY = plotCoordinates(Jx, Jy)

            if vertical:
                if gap > 0.0 and NP(NP(DX - gap/2.0) - NP(AX + gap/2.0)).min() > 0.0:
                    AX += gap/2.0
                    BX += gap/2.0
                    CX += gap/2.0
                    DX -= gap/2.0
                    EX -= gap/2.0
                    FX -= gap/2.0
            else:
                if gap > 0.0 and NP(NP(DY - gap/2.0) - NP(AY + gap/2.0)).min() > 0.0:
                    AY += gap/2.0
                    BY += gap/2.0
                    CY += gap/2.0
                    DY -= gap/2.0
                    EY -= gap/2.0
                    FY -= gap/2.0

            style = self.getStyleState()
            strokeStyle = dict((x, style[x]) for x in style if x.startswith("stroke"))
            strokeStyle["fill"] = "none"
            style = PlotStyle.toString(style)
            strokeStyle = PlotStyle.toString(strokeStyle)

            for i in xrange(len(lowEdge)):
                pathdata = ["M %r %r" % (HX[i], HY[i]),
                            "L %r %r" % (AX[i], AY[i]),
                            "L %r %r" % (BX[i], BY[i]),
                            "L %r %r" % (CX[i], CY[i]),
                            "L %r %r" % (IX[i], IY[i]),
                            "L %r %r" % (FX[i], FY[i]),
                            "L %r %r" % (EX[i], EY[i]),
                            "L %r %r" % (DX[i], DY[i]),
                            "L %r %r" % (HX[i], HY[i]),
                            "Z"]
                output.append(svg.path(d=" ".join(pathdata), style=style))
                output.append(svg.path(d="M %r %r L %r %r" % (BX[i], BY[i], EX[i], EY[i]), style=strokeStyle))
                output.append(svg.path(d="M %r %r L %r %r" % (HX[i], HY[i], GX[i], GY[i]), style=strokeStyle))
                output.append(svg.path(d="M %r %r L %r %r" % (IX[i], IY[i], JX[i], JY[i]), style=strokeStyle))

                if vertical:
                    width = (DX[i] - AX[i]) / 4.0
                    output.append(svg.path(d="M %r %r L %r %r" % (GX[i] - width, GY[i], GX[i] + width, GY[i]), style=strokeStyle))
                    output.append(svg.path(d="M %r %r L %r %r" % (JX[i] - width, JY[i], JX[i] + width, JY[i]), style=strokeStyle))
                else:
                    width = (DY[i] - AY[i]) / 4.0
                    output.append(svg.path(d="M %r %r L %r %r" % (GX[i], GY[i] - width, GX[i], GY[i] + width), style=strokeStyle))
                    output.append(svg.path(d="M %r %r L %r %r" % (JX[i], JY[i] - width, JX[i], JY[i] + width), style=strokeStyle))

        performanceTable.end("PlotBoxAndWhisker draw")

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        return output
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotBoxAndWhisker draw")

        vertical = self.get("vertical", defaultFromXsd=True, convertType=True)
        gap = self.get("gap", defaultFromXsd=True, convertType=True)

        if state.slicedFieldType is not self.fieldTypeNumeric:
            if vertical:
                strings = plotCoordinates.xstrings
            else:
                strings = plotCoordinates.ystrings

            newRanges = []
            for string in strings:
                try:
                    index = state.edges.index(string)
                except ValueError:
                    newRanges.append(None)
                else:
                    newRanges.append(state.ranges[index])

            state.ranges = newRanges
            state.edges = [(i - 0.5, i + 0.5) for i in xrange(len(strings))]

        lowEdge = NP("array", [low if low is not None else float("-inf") for low, high in state.edges], dtype=NP.dtype(float))
        highEdge = NP("array", [high if high is not None else float("inf") for low, high in state.edges], dtype=NP.dtype(float))

        selection = NP("array", [levels is not None for levels in state.ranges], dtype=NP.dtype(bool))
        lowEdge = lowEdge[selection]
        highEdge = highEdge[selection]

        lowWhisker  = NP("array", [levels[0] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        lowBox      = NP("array", [levels[1] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        midLine     = NP("array", [levels[2] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        highBox     = NP("array", [levels[3] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        highWhisker = NP("array", [levels[4] for levels in state.ranges if levels is not None], dtype=state.profiledFieldType.dtype)
        
        output = svg.g()
        if len(lowEdge) > 0:
            if vertical:
                Ax = lowEdge
                Bx = lowEdge
                Cx = lowEdge
                Dx = highEdge
                Ex = highEdge
                Fx = highEdge
                Gx = NP(NP(lowEdge + highEdge) / 2.0)
                Hx = Gx
                Ix = Gx
                Jx = Gx

                Ay = lowBox
                By = midLine
                Cy = highBox
                Dy = lowBox
                Ey = midLine
                Fy = highBox
                Gy = lowWhisker
                Hy = lowBox
                Iy = highBox
                Jy = highWhisker

            else:
                Ax = lowBox
                Bx = midLine
                Cx = highBox
                Dx = lowBox
                Ex = midLine
                Fx = highBox
                Gx = lowWhisker
                Hx = lowBox
                Ix = highBox
                Jx = highWhisker

                Ay = lowEdge
                By = lowEdge
                Cy = lowEdge
                Dy = highEdge
                Ey = highEdge
                Fy = highEdge
                Gy = NP(NP(lowEdge + highEdge) / 2.0)
                Hy = Gy
                Iy = Gy
                Jy = Gy

            AX, AY = plotCoordinates(Ax, Ay)
            BX, BY = plotCoordinates(Bx, By)
            CX, CY = plotCoordinates(Cx, Cy)
            DX, DY = plotCoordinates(Dx, Dy)
            EX, EY = plotCoordinates(Ex, Ey)
            FX, FY = plotCoordinates(Fx, Fy)
            GX, GY = plotCoordinates(Gx, Gy)
            HX, HY = plotCoordinates(Hx, Hy)
            IX, IY = plotCoordinates(Ix, Iy)
            JX, JY = plotCoordinates(Jx, Jy)

            if vertical:
                if gap > 0.0 and NP(NP(DX - gap/2.0) - NP(AX + gap/2.0)).min() > 0.0:
                    AX += gap/2.0
                    BX += gap/2.0
                    CX += gap/2.0
                    DX -= gap/2.0
                    EX -= gap/2.0
                    FX -= gap/2.0
            else:
                if gap > 0.0 and NP(NP(DY - gap/2.0) - NP(AY + gap/2.0)).min() > 0.0:
                    AY += gap/2.0
                    BY += gap/2.0
                    CY += gap/2.0
                    DY -= gap/2.0
                    EY -= gap/2.0
                    FY -= gap/2.0

            style = self.getStyleState()
            strokeStyle = dict((x, style[x]) for x in style if x.startswith("stroke"))
            strokeStyle["fill"] = "none"
            style = PlotStyle.toString(style)
            strokeStyle = PlotStyle.toString(strokeStyle)

            for i in xrange(len(lowEdge)):
                pathdata = ["M %r %r" % (HX[i], HY[i]),
                            "L %r %r" % (AX[i], AY[i]),
                            "L %r %r" % (BX[i], BY[i]),
                            "L %r %r" % (CX[i], CY[i]),
                            "L %r %r" % (IX[i], IY[i]),
                            "L %r %r" % (FX[i], FY[i]),
                            "L %r %r" % (EX[i], EY[i]),
                            "L %r %r" % (DX[i], DY[i]),
                            "L %r %r" % (HX[i], HY[i]),
                            "Z"]
                output.append(svg.path(d=" ".join(pathdata), style=style))
                output.append(svg.path(d="M %r %r L %r %r" % (BX[i], BY[i], EX[i], EY[i]), style=strokeStyle))
                output.append(svg.path(d="M %r %r L %r %r" % (HX[i], HY[i], GX[i], GY[i]), style=strokeStyle))
                output.append(svg.path(d="M %r %r L %r %r" % (IX[i], IY[i], JX[i], JY[i]), style=strokeStyle))

                if vertical:
                    width = (DX[i] - AX[i]) / 4.0
                    output.append(svg.path(d="M %r %r L %r %r" % (GX[i] - width, GY[i], GX[i] + width, GY[i]), style=strokeStyle))
                    output.append(svg.path(d="M %r %r L %r %r" % (JX[i] - width, JY[i], JX[i] + width, JY[i]), style=strokeStyle))
                else:
                    width = (DY[i] - AY[i]) / 4.0
                    output.append(svg.path(d="M %r %r L %r %r" % (GX[i], GY[i] - width, GX[i], GY[i] + width), style=strokeStyle))
                    output.append(svg.path(d="M %r %r L %r %r" % (JX[i], JY[i] - width, JX[i], JY[i] + width), style=strokeStyle))

        performanceTable.end("PlotBoxAndWhisker draw")

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        return output
Exemple #13
0
class PlotLegend(PmmlPlotContentAnnotation):
    """PlotLegend represents a plot legend that may overlay a plot or
    stand alone in an empty PlotLayout.

    The content of a PlotLegend is entered as the PMML element's text
    (like Array).  Newlines delimit rows and spaces delimit columns,
    though spaces may be included in an item by quoting the item (also
    like Array).  In addition to text, a PlotLegend can contain any
    PLOT-LEGEND-CONTENT (PmmlPlotLegendContent).
    
    To center the PlotLegend, set all margins to "auto".  To put it in
    a corner, set all margins to "auto" except for the desired corner
    (default is margin-right: -10; margin-top: -10; margin-left: auto;
    margin-bottom: auto).  To fill the area (hiding anything below
    it), set all margins to a specific value.

    PMML contents:

      - Newline and space-delimited text, as well as PLOT-LEGEND-CONTENT
        (PmmlPlotLegendContent) elements.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - style: CSS style properties.

    CSS properties:

      - margin-top, margin-right, margin-bottom, margin-left,
        margin: space between the enclosure and the border.
      - border-top-width, border-right-width, border-bottom-width,
        border-left-width, border-width: thickness of the border.
      - padding-top, padding-right, padding-bottom, padding-left,
        padding: space between the border and the inner content.
      - background, background-opacity: color of the background.
      - border-color, border-dasharray, border-dashoffset,
        border-linecap, border-linejoin, border-miterlimit,
        border-opacity, border-width: properties of the border line.
      - font, font-family, font-size, font-size-adjust, font-stretch,
        font-style, font-variant, font-weight: properties of the
        title font.

    See the source code for the full XSD.
    """

    styleProperties = [
        "margin-top",
        "margin-right",
        "margin-bottom",
        "margin-left",
        "border-top-width",
        "border-right-width",
        "border-bottom-width",
        "border-left-width",
        "border-width",
        "padding-top",
        "padding-right",
        "padding-bottom",
        "padding-left",
        "padding",
        "background",
        "background-opacity",
        "border-color",
        "border-dasharray",
        "border-dashoffset",
        "border-linecap",
        "border-linejoin",
        "border-miterlimit",
        "border-opacity",
        "border-width",
        "font",
        "font-family",
        "font-size",
        "font-size-adjust",
        "font-stretch",
        "font-style",
        "font-variant",
        "font-weight",
        "text-color",
        "column-align",
        "column-padding",
    ]

    styleDefaults = {
        "background": "white",
        "border-color": "black",
        "margin-right": "-10",
        "margin-top": "-10",
        "padding": "10",
        "border-width": "2",
        "font-size": "25.0",
        "text-color": "black",
        "column-align": "m",
        "column-padding": "30"
    }

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotLegend">
        <xs:complexType mixed="true">
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:group ref="PLOT-LEGEND-CONTENT" minOccurs="0" maxOccurs="unbounded" />
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    _re_word = re.compile(r'("(([^"]|\\")*[^\\])"|""|[^ \t"]+)',
                          (re.MULTILINE | re.UNICODE))

    def draw(self, dataTable, functionTable, performanceTable, plotCoordinates,
             plotContentBox, plotDefinitions):
        """Draw the plot annotation.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed.
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLegend")

        # figure out how to format text
        style = self.getStyleState()
        textStyle = {"fill": style["text-color"], "stroke": "none"}
        for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
            if styleProperty in style:
                textStyle[styleProperty] = style[styleProperty]
        labelAttributes = {
            "font-size": style["font-size"],
            defs.XML_SPACE: "preserve",
            "style": PlotStyle.toString(textStyle)
        }

        columnAlign = style["column-align"]
        if not set(columnAlign.lower()).issubset(set(["l", "m", "r", "."])):
            raise defs.PmmlValidationError(
                "PlotLegend's column-align style property may only contain the following characters: \"l\", \"m\", \"r\", \".\""
            )

        columnPadding = float(style["column-padding"])

        ### get an <svg:text> object for each cell

        # content follows the same delimiter logic as Array, except that lineseps outside of quotes signify new table rows
        rowIndex = 0
        colIndex = 0
        cellContents = {}
        for item in sum([[x, x.tail]
                         for x in self.childrenOfClass(PmmlPlotLegendContent)],
                        [self.text]):
            if item is None: pass

            elif isinstance(item, basestring):
                for word in re.finditer(self._re_word, item):
                    one, two, three = word.groups()

                    # quoted text; take it all as-is, without the outermost quotes and unquoting quoted quotes
                    if two is not None:
                        cellContents[rowIndex, colIndex] = svg.text(
                            two.replace(r'\"', '"'), **labelAttributes)
                        colIndex += 1

                    elif one == r'""':
                        colIndex += 1

                    else:
                        newlineIndex = one.find(os.linesep)
                        if newlineIndex == 0 and not (rowIndex == 0
                                                      and colIndex == 0):
                            rowIndex += 1
                            colIndex = 0
                        while newlineIndex != -1:
                            if one[:newlineIndex] != "":
                                cellContents[rowIndex, colIndex] = svg.text(
                                    one[:newlineIndex], **labelAttributes)
                                rowIndex += 1
                                colIndex = 0

                            one = one[(newlineIndex + len(os.linesep)):]
                            newlineIndex = one.find(os.linesep)

                        if one != "":
                            cellContents[rowIndex, colIndex] = svg.text(
                                one, **labelAttributes)
                            colIndex += 1

            else:
                performanceTable.pause("PlotLegend")
                rowIndex, colIndex = item.draw(dataTable, functionTable,
                                               performanceTable, rowIndex,
                                               colIndex, cellContents,
                                               labelAttributes,
                                               plotDefinitions)
                performanceTable.unpause("PlotLegend")

        maxRows = 0
        maxCols = 0
        maxChars = {}
        beforeDot = {}
        afterDot = {}
        for row, col in cellContents:
            if row > maxRows:
                maxRows = row
            if col > maxCols:
                maxCols = col

            if col >= len(columnAlign):
                alignment = columnAlign[-1]
            else:
                alignment = columnAlign[col]

            if col not in maxChars:
                maxChars[col] = 0
                beforeDot[col] = 0
                afterDot[col] = 0

            textContent = cellContents[row, col].text
            if textContent is not None:
                if len(textContent) > maxChars[col]:
                    maxChars[col] = len(textContent)

                if alignment == ".":
                    dotPosition = textContent.find(".")
                    if dotPosition == -1:
                        dotPosition = textContent.find("e")
                        if dotPosition == -1:
                            dotPosition = textContent.find("E")
                            if dotPosition == -1:
                                dotPosition = textContent.find(u"\u00d710")
                                if dotPosition == -1:
                                    dotPosition = len(textContent)
                    if dotPosition > beforeDot[col]:
                        beforeDot[col] = dotPosition
                    if len(textContent) - dotPosition > afterDot[col]:
                        afterDot[col] = len(textContent) - dotPosition

        maxRows += 1
        maxCols += 1
        for col in xrange(maxCols):
            if beforeDot[col] + afterDot[col] > maxChars[col]:
                maxChars[col] = beforeDot[col] + afterDot[col]
        cellWidthDenom = float(sum(maxChars.values()))

        ### create a subContentBox and fill the table cells

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        # change some of the margins based on text, unless overridden by explicit styleProperties

        if style.get("margin-bottom") == "auto": del style["margin-bottom"]
        if style.get("margin-top") == "auto": del style["margin-top"]
        if style.get("margin-left") == "auto": del style["margin-left"]
        if style.get("margin-right") == "auto": del style["margin-right"]

        subContentBox = plotContentBox.subContent(style)
        nominalHeight = maxRows * float(style["font-size"])
        nominalWidth = cellWidthDenom * 0.5 * float(
            style["font-size"]) + columnPadding * (maxCols - 1)

        if nominalHeight < subContentBox.height:
            if "margin-bottom" in style and "margin-top" in style:
                pass
            elif "margin-bottom" in style:
                style["margin-top"] = subContentBox.height - nominalHeight
            elif "margin-top" in style:
                style["margin-bottom"] = subContentBox.height - nominalHeight
            else:
                style["margin-bottom"] = style["margin-top"] = (
                    subContentBox.height - nominalHeight) / 2.0

        if nominalWidth < subContentBox.width:
            if "margin-left" in style and "margin-right" in style:
                pass
            elif "margin-left" in style:
                style["margin-right"] = subContentBox.width - nominalWidth
            elif "margin-right" in style:
                style["margin-left"] = subContentBox.width - nominalWidth
            else:
                style["margin-left"] = style["margin-right"] = (
                    subContentBox.width - nominalWidth) / 2.0

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        ### create a border rectangle
        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            if "background-opacity" in style:
                rectStyle["fill-opacity"] = style["background-opacity"]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {
                "x": repr(x1),
                "y": repr(y1),
                "width": repr(x2 - x1),
                "height": repr(y2 - y1),
                "style": PlotStyle.toString(rectStyle)
            }
            if svgId is not None:
                subAttrib["id"] = svgId + ".background"

            if rectStyle["fill"] != "none":
                content.append(svg.rect(**subAttrib))

        ### put the cell content in the table
        if subContentBox is not None:
            cellHeight = subContentBox.height / float(maxRows)
            colStart = [subContentBox.x]
            for col in xrange(maxCols):
                colStart.append(colStart[col] + subContentBox.width *
                                maxChars[col] / cellWidthDenom)

            for row in xrange(maxRows):
                for col in xrange(maxCols):
                    cellContent = cellContents.get((row, col))
                    if cellContent is not None:
                        if col >= len(columnAlign):
                            alignment = columnAlign[-1]
                        else:
                            alignment = columnAlign[col]

                        textContent = None
                        if cellContent.tag == "text" or cellContent.tag[
                                -5:] == "}text":
                            if alignment.lower() == "l":
                                cellContent.set("text-anchor", "start")
                            elif alignment.lower() == "m":
                                cellContent.set("text-anchor", "middle")
                            elif alignment.lower() == "r":
                                cellContent.set("text-anchor", "end")
                            elif alignment.lower() == ".":
                                cellContent.set("text-anchor", "middle")
                            textContent = cellContent.text

                        if alignment.lower() == ".":
                            if textContent is None:
                                alignment = "m"
                            else:
                                dotPosition = textContent.find(".")
                                if dotPosition == -1:
                                    dotPosition = textContent.find("e")
                                    if dotPosition == -1:
                                        dotPosition = textContent.find("E")
                                        if dotPosition == -1:
                                            dotPosition = textContent.find(
                                                u"\u00d710")
                                            if dotPosition == -1:
                                                dotPosition = len(
                                                    textContent) - 0.3
                                dotPosition += 0.2 * textContent[:int(
                                    math.ceil(dotPosition))].count(u"\u2212")

                                x = (colStart[col] + colStart[col + 1]) / 2.0
                                x -= (dotPosition - 0.5 * len(textContent) +
                                      0.5) * nominalWidth / cellWidthDenom

                        if alignment.lower() == "l":
                            x = colStart[col]
                        elif alignment.lower() == "m":
                            x = (colStart[col] + colStart[col + 1]) / 2.0
                        elif alignment.lower() == "r":
                            x = colStart[col + 1]

                        y = subContentBox.y + cellHeight * (row + 0.75)
                        x, y = plotCoordinates(x, y)

                        cellContent.set("transform",
                                        "translate(%r,%r)" % (x, y))
                        content.append(cellContent)

        ### create a border rectangle (reuses subAttrib, replaces subAttrib["style"])
        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace(
                            "border-", "stroke-")] = style[styleProperty]

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        performanceTable.end("PlotLegend")
        return svg.g(*content, **attrib)
    def draw(self, dataTable, functionTable, performanceTable, plotCoordinates, plotContentBox, plotDefinitions):
        """Draw the plot annotation.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed.
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker

        svgId = self.get("svgId")
        if svgId is None:
            output = svg.g()
        else:
            output = svg.g(**{"id": svgId})
        content = [output]

        inlineSvg = self.getchildren()
        fileName = self.get("fileName")
        if len(inlineSvg) == 1 and fileName is None:
            svgBinding = inlineSvg[0]
        elif len(inlineSvg) == 0 and fileName is not None:
            svgBinding = SvgBinding.loadXml(fileName)
        else:
            raise defs.PmmlValidationError(
                "PlotSvgAnnotation should specify an inline SVG or a fileName but not both or neither"
            )

        style = self.getStyleState()

        if style.get("margin-bottom") == "auto":
            del style["margin-bottom"]
        if style.get("margin-top") == "auto":
            del style["margin-top"]
        if style.get("margin-left") == "auto":
            del style["margin-left"]
        if style.get("margin-right") == "auto":
            del style["margin-right"]

        subContentBox = plotContentBox.subContent(style)
        sx1, sy1, sx2, sy2 = PlotSvgAnnotation.findSize(svgBinding)
        nominalHeight = sy2 - sy1
        nominalWidth = sx2 - sx1

        if nominalHeight < subContentBox.height:
            if "margin-bottom" in style and "margin-top" in style:
                pass
            elif "margin-bottom" in style:
                style["margin-top"] = subContentBox.height - nominalHeight
            elif "margin-top" in style:
                style["margin-bottom"] = subContentBox.height - nominalHeight
            else:
                style["margin-bottom"] = style["margin-top"] = (subContentBox.height - nominalHeight) / 2.0

        if nominalWidth < subContentBox.width:
            if "margin-left" in style and "margin-right" in style:
                pass
            elif "margin-left" in style:
                style["margin-right"] = subContentBox.width - nominalWidth
            elif "margin-right" in style:
                style["margin-left"] = subContentBox.width - nominalWidth
            else:
                style["margin-left"] = style["margin-right"] = (subContentBox.width - nominalWidth) / 2.0

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        if subContentBox is not None:
            tx1, ty1 = plotCoordinates(subContentBox.x, subContentBox.y)
            tx2, ty2 = plotCoordinates(subContentBox.x + subContentBox.width, subContentBox.y + subContentBox.height)

            output.extend([copy.deepcopy(x) for x in svgBinding.getchildren()])

            output["transform"] = "translate(%r, %r) scale(%r, %r)" % (
                tx1 - sx1,
                ty1 - sy1,
                (tx2 - tx1) / float(sx2 - sx1),
                (ty2 - ty1) / float(sy2 - sy1),
            )

        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in (
                    "border-dasharray",
                    "border-dashoffset",
                    "border-linecap",
                    "border-linejoin",
                    "border-miterlimit",
                    "border-opacity",
                    "border-width",
                ):
                    if styleProperty in style:
                        rectStyle[styleProperty.replace("border-", "stroke-")] = style[styleProperty]

                x1 = borderRect.x
                y1 = borderRect.y
                x2 = borderRect.x + borderRect.width
                y2 = borderRect.y + borderRect.height
                x1, y1 = plotCoordinates(x1, y1)
                x2, y2 = plotCoordinates(x2, y2)

                subAttrib = {
                    "x": repr(x1),
                    "y": repr(y1),
                    "width": repr(x2 - x1),
                    "height": repr(y2 - y1),
                    "style": PlotStyle.toString(rectStyle),
                }

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        return svg.g(*content)
Exemple #15
0
class PlotGuideLines(PmmlPlotContent):
    """Represents a set of guide lines to help interpret the plot.

    PMML subelements:

      - PlotVerticalLines: infinite set of vertical lines to draw,
        usually used as part of a background grid.
      - PlotHorizontalLines: infinite set of horizontal lines to
        draw, usually used as part of a background grid.
      - PlotLine: arbitrary line used to call out a feature on a
        plot.  One of its endpoints may be at infinity.

    PMML attributes:

      - svgId: id for the resulting SVG element.

    CSS properties:
      - stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap,
        stroke-linejoin, stroke-miterlimit, stroke-opacity,
        stroke-width: properties of the line drawing.

    See the source code for the full XSD.
    """

    styleProperties = [
        "stroke",
        "stroke-dasharray",
        "stroke-dashoffset",
        "stroke-linecap",
        "stroke-linejoin",
        "stroke-miterlimit",
        "stroke-opacity",
        "stroke-width",
    ]

    styleDefaults = {"stroke": "black"}

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotGuideLines">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:choice minOccurs="1" maxOccurs="unbounded">
                    <xs:element ref="PlotVerticalLines" minOccurs="1" maxOccurs="1" />
                    <xs:element ref="PlotHorizontalLines" minOccurs="1" maxOccurs="1" />
                    <xs:element ref="PlotLine" minOccurs="1" maxOccurs="1" />
                </xs:choice>
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
        </xs:complexType>
    </xs:element>
</xs:schema>
"""

    xsdRemove = ["PlotVerticalLines", "PlotHorizontalLines", "PlotLine"]

    xsdAppend = [
        """<xs:element name="PlotVerticalLines" xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
            </xs:sequence>
            <xs:attribute name="x0" type="xs:string" use="required" />
            <xs:attribute name="spacing" type="xs:double" use="required" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
""" % PlotStyle.toString(styleDefaults),
        """<xs:element name="PlotHorizontalLines" xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
            </xs:sequence>
            <xs:attribute name="y0" type="xs:string" use="required" />
            <xs:attribute name="spacing" type="xs:double" use="required" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
""" % PlotStyle.toString(styleDefaults),
        """<xs:element name="PlotLine" xmlns:xs="http://www.w3.org/2001/XMLSchema">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
            </xs:sequence>
            <xs:attribute name="x1" type="xs:string" use="required" />
            <xs:attribute name="y1" type="xs:string" use="required" />
            <xs:attribute name="x2" type="xs:string" use="required" />
            <xs:attribute name="y2" type="xs:string" use="required" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>""" % PlotStyle.toString(styleDefaults)
    ]

    def prepare(self, state, dataTable, functionTable, performanceTable,
                plotRange):
        """Prepare a plot element for drawing.

        This stage consists of calculating all quantities and
        determing the bounds of the data.  These bounds may be unioned
        with bounds from other plot elements that overlay this plot
        element, so the drawing (which requires a finalized coordinate
        system) cannot begin yet.

        This method modifies C{plotRange}.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotRange: PlotRange
        @param plotRange: The bounding box of plot coordinates that this function will expand.
        """

        self._saveContext(dataTable)

        for directive in self.xpath("pmml:PlotLine"):
            try:
                x1 = float(directive["x1"])
                y1 = float(directive["y1"])
                x2 = float(directive["x2"])
                y2 = float(directive["y2"])
            except ValueError:
                pass
            else:
                fieldType = FakeFieldType("double", "continuous")
                plotRange.xminPush(x1, fieldType, sticky=False)
                plotRange.yminPush(y1, fieldType, sticky=False)
                plotRange.xmaxPush(x2, fieldType, sticky=False)
                plotRange.ymaxPush(y2, fieldType, sticky=False)

    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotGuideLines draw")

        output = svg.g()

        for directive in self.xpath(
                "pmml:PlotVerticalLines | pmml:PlotHorizontalLines | pmml:PlotLine"
        ):
            style = dict(self.styleDefaults)
            currentStyle = directive.get("style")
            if currentStyle is not None:
                style.update(PlotStyle.toDict(currentStyle))
            style["fill"] = "none"
            style = PlotStyle.toString(style)

            if directive.hasTag("PlotVerticalLines"):
                try:
                    x0 = plotCoordinates.xfieldType.stringToValue(
                        directive["x0"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid x0: %r" %
                                                   directive["x0"])

                spacing = float(directive["spacing"])
                low = plotCoordinates.innerX1
                high = plotCoordinates.innerX2

                up = list(
                    NP("arange", x0, high, spacing, dtype=NP.dtype(float)))
                down = list(
                    NP("arange",
                       x0 - spacing,
                       low,
                       -spacing,
                       dtype=NP.dtype(float)))

                for x in up + down:
                    x1, y1 = x, float("-inf")
                    X1, Y1 = plotCoordinates(x1, y1)
                    x2, y2 = x, float("inf")
                    X2, Y2 = plotCoordinates(x2, y2)

                    output.append(
                        svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2),
                                 style=style))

            elif directive.hasTag("PlotHorizontalLines"):
                try:
                    y0 = plotCoordinates.xfieldType.stringToValue(
                        directive["y0"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid y0: %r" %
                                                   directive["y0"])

                spacing = float(directive["spacing"])
                low = plotCoordinates.innerY1
                high = plotCoordinates.innerY2

                up = list(
                    NP("arange", y0, high, spacing, dtype=NP.dtype(float)))
                down = list(
                    NP("arange",
                       y0 - spacing,
                       low,
                       -spacing,
                       dtype=NP.dtype(float)))

                for y in up + down:
                    x1, y1 = float("-inf"), y
                    X1, Y1 = plotCoordinates(x1, y1)
                    x2, y2 = float("inf"), y
                    X2, Y2 = plotCoordinates(x2, y2)

                    output.append(
                        svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2),
                                 style=style))

            elif directive.hasTag("PlotLine"):
                try:
                    x1 = plotCoordinates.xfieldType.stringToValue(
                        directive["x1"])
                    y1 = plotCoordinates.xfieldType.stringToValue(
                        directive["y1"])
                    x2 = plotCoordinates.xfieldType.stringToValue(
                        directive["x2"])
                    y2 = plotCoordinates.xfieldType.stringToValue(
                        directive["y2"])
                except ValueError:
                    raise defs.PmmlValidationError(
                        "Invalid x1, y1, x2, or y2: %r %r %r %r" %
                        (directive["x1"], directive["y1"], directive["x2"],
                         directive["y2"]))

                X1, Y1 = plotCoordinates(x1, y1)
                X2, Y2 = plotCoordinates(x2, y2)

                output.append(
                    svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2),
                             style=style))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotGuideLines draw")

        return output
Exemple #16
0
class PlotCurve(PmmlPlotContent):
    """Represents a curve defined by mathematical formulae or a jagged
    line/smooth curve through a set of data points.

    PMML subelements for a 1d formula:

      - PlotFormula role="y(x)"
      - PlotFormula role="dy/dx" (optional)

    PMML subelements for a parametric formula:

      - PlotFormula role="x(t)"
      - PlotFormula role="y(t)"
      - PlotFormula role="dx/dt" (optional)
      - PlotFormula role="dy/dt" (optional)

    PMML subelements for a fit to data points:

      - PlotNumericExpression role="x"
      - PlotNumericExpression role="y"
      - PlotNumericExpression role="dx" (optional)
      - PlotNumericExpression role="dy" (optional)
      - PlotSelection (optional)

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - stateId: key for persistent storage in a DataTableState.
      - low: low edge of domain (in x or t) for mathematical
        formulae.
      - high: high edge of domain (in x or t) for mathematical
        formulae.
      - numSamples: number of locations to sample for mathematical
        formulae.
      - samplingMethod: "uniform", "random", or "adaptive".
      - loop: if "true", draw a closed loop that connects the first
        and last points.
      - smooth: if "false", draw a jagged line between each data
        point; if "true", fit a smooth curve.
      - smoothingScale: size of the smoothing scale in units of the
        domain (in x or t).
      - style: CSS style properties.

    CSS properties:
      - fill, fill-opacity: color under the curve.
      - stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap,
        stroke-linejoin, stroke-miterlimit, stroke-opacity,
        stroke-width: properties of the line drawing.

    See the source code for the full XSD.
    """

    styleProperties = [
        "fill",
        "fill-opacity",
        "stroke",
        "stroke-dasharray",
        "stroke-dashoffset",
        "stroke-linecap",
        "stroke-linejoin",
        "stroke-miterlimit",
        "stroke-opacity",
        "stroke-width",
    ]

    styleDefaults = {"fill": "none", "stroke": "black"}

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotCurve">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:choice minOccurs="1" maxOccurs="1">
                    <xs:element ref="PlotFormula" minOccurs="1" maxOccurs="4" />
                    <xs:sequence>
                        <xs:element ref="PlotNumericExpression" minOccurs="1" maxOccurs="4" />
                        <xs:element ref="PlotSelection" minOccurs="0" maxOccurs="1" />
                    </xs:sequence>
                </xs:choice>
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="stateId" type="xs:string" use="optional" />
            <xs:attribute name="low" type="xs:double" use="optional" />
            <xs:attribute name="high" type="xs:double" use="optional" />
            <xs:attribute name="numSamples" type="xs:positiveInteger" use="optional" default="100" />
            <xs:attribute name="samplingMethod" use="optional" default="uniform">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                        <xs:enumeration value="uniform" />
                        <xs:enumeration value="random" />
                        <xs:enumeration value="adaptive" />
                    </xs:restriction>
                </xs:simpleType>
            </xs:attribute>
            <xs:attribute name="loop" type="xs:boolean" use="optional" default="false" />
            <xs:attribute name="smooth" type="xs:boolean" use="optional" default="true" />
            <xs:attribute name="smoothingScale" type="xs:double" use="optional" default="1.0" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    xfieldType = FakeFieldType("double", "continuous")

    @classmethod
    def expressionsToPoints(cls, expression, derivative, samples, loop,
                            functionTable, performanceTable):
        """Evaluate a set of given string-based formulae to generate
        numeric points.

        This is used to plot mathematical curves.

        @type expression: 1- or 2-tuple of strings
        @param expression: If a 1-tuple, the string is passed to Formula and interpreted as y(x); if a 2-tuple, the strings are passed to Formula and interpreted as x(t), y(t).
        @type derivative: 1- or 2-tuple of strings (same length as C{expression})
        @param derivative: Strings are passed to Formua and interpreted as dy/dx (if a 1-tuple) or dx/dt, dy/dt (if a 2-tuple).
        @type samples: 1d Numpy array
        @param samples: Values of x or t at which to evaluate the expression or expressions.
        @type loop: bool
        @param loop: If False, disconnect the end of the set of points from the beginning.
        @type functionTable: FunctionTable
        @param functionTable: Functions that may be used to perform the calculation.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the process.
        @rtype: 6-tuple
        @return: C{xlist}, C{ylist}, C{dxlist}, C{dylist} (1d Numpy arrays), xfieldType, yfieldType (FieldTypes).
        """

        if len(expression) == 1:
            sampleTable = DataTable({"x": "double"}, {"x": samples})

            parsed = Formula.parse(expression[0])
            ydataColumn = parsed.evaluate(sampleTable, functionTable,
                                          performanceTable)
            if not ydataColumn.fieldType.isnumeric(
            ) and not ydataColumn.fieldType.istemporal():
                raise defs.PmmlValidationError(
                    "PlotFormula y(x) must return a numeric expression, not %r"
                    % ydataColumn.fieldType)

            xfieldType = cls.xfieldType
            yfieldType = ydataColumn.fieldType

            selection = None
            if ydataColumn.mask is not None:
                selection = NP(ydataColumn.mask == defs.VALID)

            if derivative[0] is None:
                if selection is None:
                    xlist = samples
                    ylist = ydataColumn.data
                else:
                    xlist = samples[selection]
                    ylist = ydataColumn.data[selection]

                dxlist = NP(
                    (NP("roll", xlist, -1) - NP("roll", xlist, 1)) / 2.0)
                dylist = NP(
                    (NP("roll", ylist, -1) - NP("roll", ylist, 1)) / 2.0)
                if not loop:
                    dxlist[0] = 0.0
                    dxlist[-1] = 0.0
                    dylist[0] = 0.0
                    dylist[-1] = 0.0

            else:
                parsed = Formula.parse(derivative[0])
                dydataColumn = parsed.evaluate(sampleTable, functionTable,
                                               performanceTable)
                if not dydataColumn.fieldType.isnumeric(
                ) and not dydataColumn.fieldType.istemporal():
                    raise defs.PmmlValidationError(
                        "PlotFormula dy/dx must return a numeric expression, not %r"
                        % dydataColumn.fieldType)

                if dydataColumn.mask is not None:
                    if selection is None:
                        selection = NP(dydataColumn.mask == defs.VALID)
                    else:
                        NP("logical_and", selection,
                           NP(dydataColumn.mask == defs.VALID), selection)

                if selection is None:
                    xlist = samples
                    ylist = ydataColumn.data
                    dxlist = NP(
                        (NP("roll", xlist, -1) - NP("roll", xlist, 1)) / 2.0)
                    dylist = dydataColumn.data
                else:
                    xlist = samples[selection]
                    ylist = ydataColumn.data[selection]
                    dxlist = NP(
                        (NP("roll", xlist, -1) - NP("roll", xlist, 1)) / 2.0)
                    dylist = NP(dydataColumn.data[selection] * dxlist)

                if not loop:
                    dxlist[0] = 0.0
                    dxlist[-1] = 0.0
                    dylist[0] = 0.0
                    dylist[-1] = 0.0

        elif len(expression) == 2:
            sampleTable = DataTable({"t": "double"}, {"t": samples})

            parsed = Formula.parse(expression[0])
            xdataColumn = parsed.evaluate(sampleTable, functionTable,
                                          performanceTable)
            if not xdataColumn.fieldType.isnumeric(
            ) and not xdataColumn.fieldType.istemporal():
                raise defs.PmmlValidationError(
                    "PlotFormula x(t) must return a numeric expression, not %r"
                    % xdataColumn.fieldType)

            parsed = Formula.parse(expression[1])
            ydataColumn = parsed.evaluate(sampleTable, functionTable,
                                          performanceTable)
            if not ydataColumn.fieldType.isnumeric(
            ) and not ydataColumn.fieldType.istemporal():
                raise defs.PmmlValidationError(
                    "PlotFormula y(t) must return a numeric expression, not %r"
                    % ydataColumn.fieldType)

            xfieldType = xdataColumn.fieldType
            yfieldType = ydataColumn.fieldType

            selection = None
            if xdataColumn.mask is not None:
                selection = NP(xdataColumn.mask == defs.VALID)
            if ydataColumn.mask is not None:
                if selection is None:
                    selection = NP(ydataColumn.mask == defs.VALID)
                else:
                    NP("logical_and", selection,
                       NP(ydataColumn.mask == defs.VALID), selection)

            if derivative[0] is None:
                if selection is None:
                    xlist = xdataColumn.data
                    ylist = ydataColumn.data
                else:
                    xlist = xdataColumn.data[selection]
                    ylist = ydataColumn.data[selection]

                dxlist = NP(
                    (NP("roll", xlist, -1) - NP("roll", xlist, 1)) / 2.0)
                dylist = NP(
                    (NP("roll", ylist, -1) - NP("roll", ylist, 1)) / 2.0)
                if not loop:
                    dxlist[0] = 0.0
                    dxlist[-1] = 0.0
                    dylist[0] = 0.0
                    dylist[-1] = 0.0

            else:
                parsed = Formula.parse(derivative[0])
                dxdataColumn = parsed.evaluate(sampleTable, functionTable,
                                               performanceTable)
                if not dxdataColumn.fieldType.isnumeric(
                ) and not dxdataColumn.fieldType.istemporal():
                    raise defs.PmmlValidationError(
                        "PlotFormula dx/dt must return a numeric expression, not %r"
                        % dxdataColumn.fieldType)

                parsed = Formula.parse(derivative[1])
                dydataColumn = parsed.evaluate(sampleTable, functionTable,
                                               performanceTable)
                if not dydataColumn.fieldType.isnumeric(
                ) and not dydataColumn.fieldType.istemporal():
                    raise defs.PmmlValidationError(
                        "PlotFormula dy/dt must return a numeric expression, not %r"
                        % dydataColumn.fieldType)

                if dxdataColumn.mask is not None:
                    if selection is None:
                        selection = NP(dxdataColumn.mask == defs.VALID)
                    else:
                        NP("logical_and", selection,
                           NP(dxdataColumn.mask == defs.VALID), selection)

                if dydataColumn.mask is not None:
                    if selection is None:
                        selection = NP(dydataColumn.mask == defs.VALID)
                    else:
                        NP("logical_and", selection,
                           NP(dydataColumn.mask == defs.VALID), selection)

                if selection is None:
                    dt = NP(
                        (NP("roll", samples, -1) - NP("roll", samples, 1)) /
                        2.0)

                    xlist = xdataColumn.data
                    ylist = ydataColumn.data
                    dxlist = NP(dxdataColumn.data * dt)
                    dylist = NP(dydataColumn.data * dt)
                else:
                    dt = NP((NP("roll", samples[selection], -1) -
                             NP("roll", samples[selection], 1)) / 2.0)

                    xlist = xdataColumn.data[selection]
                    ylist = ydataColumn.data[selection]
                    dxlist = NP(dxdataColumn.data[selection] * dt)
                    dylist = NP(dydataColumn.data[selection] * dt)

                if not loop:
                    dxlist[0] = 0.0
                    dxlist[-1] = 0.0
                    dylist[0] = 0.0
                    dylist[-1] = 0.0

        return xlist, ylist, dxlist, dylist, xfieldType, yfieldType

    @staticmethod
    def pointsToSmoothCurve(xarray, yarray, samples, smoothingScale, loop):
        """Fit a smooth line through a set of given numeric points
        with a characteristic smoothing scale.

        This is a non-parametric locally linear fit, used to plot data
        as a smooth line.

        @type xarray: 1d Numpy array of numbers
        @param xarray: Array of x values.
        @type yarray: 1d Numpy array of numbers
        @param yarray: Array of y values.
        @type samples: 1d Numpy array of numbers
        @param samples: Locations at which to fit the C{xarray} and C{yarray} with best-fit positions and derivatives.
        @type smoothingScale: number
        @param smoothingScale: Standard deviation of the Gaussian kernel used to smooth the locally linear fit.
        @type loop: bool
        @param loop: If False, disconnect the end of the fitted curve from the beginning.
        @rtype: 4-tuple of 1d Numpy arrays
        @return: C{xlist}, C{ylist}, C{dxlist}, C{dylist} appropriate for C{formatPathdata}.
        """

        ylist = []
        dylist = []

        for sample in samples:
            weights = NP(
                NP(
                    NP(
                        "exp",
                        NP(
                            NP(-0.5 * NP("power", NP(xarray - sample), 2)) /
                            NP(smoothingScale * smoothingScale))) /
                    smoothingScale) / (math.sqrt(2.0 * math.pi)))
            sum1 = weights.sum()
            sumx = NP(weights * xarray).sum()
            sumxx = NP(weights * NP(xarray * xarray)).sum()
            sumy = NP(weights * yarray).sum()
            sumxy = NP(weights * NP(xarray * yarray)).sum()

            delta = (sum1 * sumxx) - (sumx * sumx)
            intercept = ((sumxx * sumy) - (sumx * sumxy)) / delta
            slope = ((sum1 * sumxy) - (sumx * sumy)) / delta

            ylist.append(intercept + (sample * slope))
            dylist.append(slope)

        xlist = samples
        ylist = NP("array", ylist, dtype=NP.dtype(float))
        dxlist = NP((NP("roll", xlist, -1) - NP("roll", xlist, 1)) / 2.0)
        dylist = NP("array", dylist, dtype=NP.dtype(float)) * dxlist
        if not loop:
            dxlist[0] = 0.0
            dxlist[-1] = 0.0
            dylist[0] = 0.0
            dylist[-1] = 0.0

        return xlist, ylist, dxlist, dylist

    @staticmethod
    def formatPathdata(xlist, ylist, dxlist, dylist, plotCoordinates, loop,
                       smooth):
        """Compute SVG path data from position and derivatives lists.

        @type xlist: 1d Numpy array of numbers
        @param xlist: Array of x values at each point t.
        @type ylist: 1d Numpy array of numbers
        @param ylist: Array of y values at each point t.
        @type dxlist: 1d Numpy array of numbers
        @param dxlist: Array of dx/dt derivatives at each point t.
        @type dylist: 1d Numpy array of numbers
        @param dylist: Array of dy/dt derivatives at each point t.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: Coordinate system to convert the points.
        @type loop: bool
        @param loop: If True, the last point should be connected to the first point.
        @type smooth: bool
        @param smooth: If True, use the derivatives (C{dxlist} and C{dylist}) to define Bezier curves between the points; otherwise, draw straight lines.
        @rtype: list of strings
        @return: When concatenated with spaces, the return type is appropriate for an SVG path's C{d} attribute.
        """

        pathdata = []
        if not smooth:
            X, Y = plotCoordinates(xlist, ylist)

            nextIsMoveto = True
            for x, y in itertools.izip(X, Y):
                if nextIsMoveto:
                    pathdata.append("M %r %r" % (x, y))
                    nextIsMoveto = False
                else:
                    pathdata.append("L %r %r" % (x, y))

            if loop:
                pathdata.append("Z")

        else:
            C1x = NP("roll", xlist, 1) + NP("roll", dxlist, 1) / 3.0
            C1y = NP("roll", ylist, 1) + NP("roll", dylist, 1) / 3.0
            C2x = xlist - dxlist / 3.0
            C2y = ylist - dylist / 3.0

            X, Y = plotCoordinates(xlist, ylist)
            C1X, C1Y = plotCoordinates(C1x, C1y)
            C2X, C2Y = plotCoordinates(C2x, C2y)

            nextIsMoveto = True
            for x, y, c1x, c1y, c2x, c2y in itertools.izip(
                    X, Y, C1X, C1Y, C2X, C2Y):
                if nextIsMoveto:
                    pathdata.append("M %r %r" % (x, y))
                    nextIsMoveto = False
                else:
                    pathdata.append("C %r %r %r %r %r %r" %
                                    (c1x, c1y, c2x, c2y, x, y))

            if loop:
                pathdata.append("Z")

        return pathdata

    def generateSamples(self, low, high):
        """Used by C{prepare} to generate an array of samples.

        @type low: number
        @param low: Minimum value to sample.
        @type high: number
        @param high: Maximum value to sample.
        @rtype: 1d Numpy array
        @return: An array of uniform, random, or adaptive samples of an interval.
        """

        numSamples = self.get("numSamples",
                              defaultFromXsd=True,
                              convertType=True)
        samplingMethod = self.get("samplingMethod", defaultFromXsd=True)

        if samplingMethod == "uniform":
            samples = NP("linspace", low, high, numSamples, endpoint=True)

        elif samplingMethod == "random":
            samples = NP(
                NP(NP(NP.random.rand(numSamples)) * (high - low)) + low)
            samples.sort()

        else:
            raise NotImplementedError("TODO: add 'adaptive'")

        return samples

    def prepare(self, state, dataTable, functionTable, performanceTable,
                plotRange):
        """Prepare a plot element for drawing.

        This stage consists of calculating all quantities and
        determing the bounds of the data.  These bounds may be unioned
        with bounds from other plot elements that overlay this plot
        element, so the drawing (which requires a finalized coordinate
        system) cannot begin yet.

        This method modifies C{plotRange}.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotRange: PlotRange
        @param plotRange: The bounding box of plot coordinates that this function will expand.
        """

        self.checkRoles([
            "y(x)", "dy/dx", "x(t)", "y(t)", "dx/dt", "dy/dt", "x", "y", "dx",
            "dy"
        ])

        performanceTable.begin("PlotCurve prepare")
        self._saveContext(dataTable)

        yofx = self.xpath("pmml:PlotFormula[@role='y(x)']")
        dydx = self.xpath("pmml:PlotFormula[@role='dy/dx']")

        xoft = self.xpath("pmml:PlotFormula[@role='x(t)']")
        yoft = self.xpath("pmml:PlotFormula[@role='y(t)']")
        dxdt = self.xpath("pmml:PlotFormula[@role='dx/dt']")
        dydt = self.xpath("pmml:PlotFormula[@role='dy/dt']")

        nx = self.xpath("pmml:PlotNumericExpression[@role='x']")
        ny = self.xpath("pmml:PlotNumericExpression[@role='y']")
        ndx = self.xpath("pmml:PlotNumericExpression[@role='dx']")
        ndy = self.xpath("pmml:PlotNumericExpression[@role='dy']")
        cutExpression = self.xpath("pmml:PlotSelection")

        if len(yofx) + len(dydx) + len(xoft) + len(yoft) + len(dxdt) + len(
                dydt) > 0:
            if len(yofx) == 1 and len(dydx) == 0 and len(xoft) == 0 and len(
                    yoft) == 0 and len(dxdt) == 0 and len(dydt) == 0:
                expression = (yofx[0].text, )
                derivative = (None, )

            elif len(yofx) == 1 and len(dydx) == 1 and len(xoft) == 0 and len(
                    yoft) == 0 and len(dxdt) == 0 and len(dydt) == 0:
                expression = (yofx[0].text, )
                derivative = (dydx[0].text, )

            elif len(yofx) == 0 and len(dydx) == 0 and len(xoft) == 1 and len(
                    yoft) == 1 and len(dxdt) == 0 and len(dydt) == 0:
                expression = xoft[0].text, yoft[0].text
                derivative = None, None

            elif len(yofx) == 0 and len(dydx) == 0 and len(xoft) == 1 and len(
                    yoft) == 1 and len(dxdt) == 1 and len(dydt) == 1:
                expression = xoft[0].text, yoft[0].text
                derivative = dxdt[0].text, dydt[0].text

            else:
                raise defs.PmmlValidationError(
                    "The only allowed combinations of PlotFormulae are: \"y(x)\", \"y(x) dy/dx\", \"x(t) y(t)\", and \"x(t) y(t) dx/dt dy/dt\""
                )

            low = self.get("low", convertType=True)
            high = self.get("high", convertType=True)
            if low is None or high is None:
                raise defs.PmmlValidationError(
                    "The \"low\" and \"high\" attributes are required for PlotCurves defined by formulae"
                )

            samples = self.generateSamples(low, high)

            loop = self.get("loop", defaultFromXsd=True, convertType=True)
            state.x, state.y, state.dx, state.dy, xfieldType, yfieldType = self.expressionsToPoints(
                expression, derivative, samples, loop, functionTable,
                performanceTable)

        else:
            performanceTable.pause("PlotCurve prepare")
            if len(ndx) == 1:
                dxdataColumn = ndx[0].evaluate(dataTable, functionTable,
                                               performanceTable)
            else:
                dxdataColumn = None
            if len(ndy) == 1:
                dydataColumn = ndy[0].evaluate(dataTable, functionTable,
                                               performanceTable)
            else:
                dydataColumn = None
            performanceTable.unpause("PlotCurve prepare")

            if len(nx) == 0 and len(ny) == 1:
                performanceTable.pause("PlotCurve prepare")
                ydataColumn = ny[0].evaluate(dataTable, functionTable,
                                             performanceTable)
                performanceTable.unpause("PlotCurve prepare")

                if len(cutExpression) == 1:
                    performanceTable.pause("PlotCurve prepare")
                    selection = cutExpression[0].select(
                        dataTable, functionTable, performanceTable)
                    performanceTable.unpause("PlotCurve prepare")
                else:
                    selection = NP("ones", len(ydataColumn.data),
                                   NP.dtype(bool))

                if ydataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(ydataColumn.mask == defs.VALID),
                                   selection)
                if dxdataColumn is not None and dxdataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(dxdataColumn.mask == defs.VALID),
                                   selection)
                if dydataColumn is not None and dydataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(dydataColumn.mask == defs.VALID),
                                   selection)

                yarray = ydataColumn.data[selection]

                xarray = NP("ones", len(yarray), dtype=NP.dtype(float))
                xarray[0] = 0.0
                xarray = NP("cumsum", xarray)

                dxarray, dyarray = None, None
                if dxdataColumn is not None:
                    dxarray = dxdataColumn.data[selection]
                if dydataColumn is not None:
                    dyarray = dydataColumn.data[selection]

                xfieldType = self.xfieldType
                yfieldType = ydataColumn.fieldType

            elif len(nx) == 1 and len(ny) == 1:
                performanceTable.pause("PlotCurve prepare")
                xdataColumn = nx[0].evaluate(dataTable, functionTable,
                                             performanceTable)
                ydataColumn = ny[0].evaluate(dataTable, functionTable,
                                             performanceTable)
                performanceTable.unpause("PlotCurve prepare")

                if len(cutExpression) == 1:
                    performanceTable.pause("PlotCurve prepare")
                    selection = cutExpression[0].select(
                        dataTable, functionTable, performanceTable)
                    performanceTable.unpause("PlotCurve prepare")
                else:
                    selection = NP("ones", len(ydataColumn.data),
                                   NP.dtype(bool))

                if xdataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(xdataColumn.mask == defs.VALID),
                                   selection)
                if ydataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(ydataColumn.mask == defs.VALID),
                                   selection)
                if dxdataColumn is not None and dxdataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(dxdataColumn.mask == defs.VALID),
                                   selection)
                if dydataColumn is not None and dydataColumn.mask is not None:
                    selection = NP("logical_and", selection,
                                   NP(dydataColumn.mask == defs.VALID),
                                   selection)

                xarray = xdataColumn.data[selection]
                yarray = ydataColumn.data[selection]

                dxarray, dyarray = None, None
                if dxdataColumn is not None:
                    dxarray = dxdataColumn.data[selection]
                if dydataColumn is not None:
                    dyarray = dydataColumn.data[selection]

                xfieldType = xdataColumn.fieldType
                yfieldType = ydataColumn.fieldType

            else:
                raise defs.PmmlValidationError(
                    "The only allowed combinations of PlotNumericExpressions are: \"y(x)\" and \"x(t) y(t)\""
                )

            persistentState = {}
            stateId = self.get("stateId")
            if stateId is not None:
                if stateId in dataTable.state:
                    persistentState = dataTable.state[stateId]
                    xarray = NP("concatenate", [xarray, persistentState["x"]])
                    yarray = NP("concatenate", [yarray, persistentState["y"]])
                    if dxarray is not None:
                        dxarray = NP("concatenate",
                                     [dxarray, persistentState["dx"]])
                    if dyarray is not None:
                        dyarray = NP("concatenate",
                                     [dyarray, persistentState["dy"]])
                else:
                    dataTable.state[stateId] = persistentState

            persistentState["x"] = xarray
            persistentState["y"] = yarray
            if dxarray is not None:
                persistentState["dx"] = dxarray
            if dyarray is not None:
                persistentState["dy"] = dyarray

            smooth = self.get("smooth", defaultFromXsd=True, convertType=True)
            if not smooth:
                if dyarray is not None and dxarray is None:
                    dxarray = NP(
                        (NP("roll", xarray, -1) - NP("roll", xarray, 1)) / 2.0)
                    dyarray = dyarray * dxarray

                loop = self.get("loop", defaultFromXsd=True, convertType=True)
                if dxarray is not None and not loop:
                    dxarray[0] = 0.0
                    dxarray[-1] = 0.0
                if dyarray is not None and not loop:
                    dyarray[0] = 0.0
                    dyarray[-1] = 0.0

                state.x = xarray
                state.y = yarray
                state.dx = dxarray
                state.dy = dyarray

            else:
                smoothingScale = self.get("smoothingScale",
                                          defaultFromXsd=True,
                                          convertType=True)
                loop = self.get("loop", defaultFromXsd=True, convertType=True)

                samples = self.generateSamples(xarray.min(), xarray.max())
                state.x, state.y, state.dx, state.dy = self.pointsToSmoothCurve(
                    xarray, yarray, samples, smoothingScale, loop)

        if plotRange is not None:
            plotRange.expand(state.x, state.y, xfieldType, yfieldType)

        performanceTable.end("PlotCurve prepare")

    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotCurve draw")

        loop = self.get("loop", defaultFromXsd=True, convertType=True)
        pathdata = self.formatPathdata(
            state.x, state.y, state.dx, state.dy, plotCoordinates, loop,
            (state.dx is not None and state.dy is not None))
        output = svg.g()

        style = self.getStyleState()
        strokeStyle = dict(
            (x, style[x]) for x in style if x.startswith("stroke"))
        fillStyle = dict((x, style[x]) for x in style if x.startswith("fill"))
        fillStyle["stroke"] = "none"

        if style["fill"] != "none":
            if len(self.xpath("pmml:PlotFormula[@role='y(x)']")) > 0 and len(
                    pathdata) > 1:
                firstPoint = plotCoordinates(state.x[0], 0.0)
                lastPoint = plotCoordinates(state.x[-1], 0.0)

                X0, Y0 = plotCoordinates(state.x[0], state.y[0])

                pathdata2 = ["M %r %r" % firstPoint]
                pathdata2.append("L %r %r" % (X0, Y0))
                pathdata2.extend(pathdata[1:])
                pathdata2.append("L %r %r" % lastPoint)

                output.append(
                    svg.path(d=" ".join(pathdata2),
                             style=PlotStyle.toString(fillStyle)))

            else:
                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(fillStyle)))

        output.append(
            svg.path(d=" ".join(pathdata),
                     style=PlotStyle.toString(strokeStyle)))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotCurve draw")
        return output
Exemple #17
0
 def style(self):
     s = self.get("style")
     if s is None:
         s = self.get("style", defaultFromXsd=True)
         self.set("style", s)
     return PlotStyle(self)
Exemple #18
0
class PlotWindow(PmmlPlotFrame):
    """PlotWindow represents a plot with four borders, tick marks, and
    optional axis labels on each.
    
    It uses a CSS margin, border, and padding to define the plot area,
    which means that axis labels extend into the margins and ticks
    extend into the padding, unlike most uses of the
    U{CSS box model<http://www.w3.org/TR/CSS2/box.html>}.
    The default left and bottom margins are large enough to accomodate
    these graphics.

    PMML subelements:

      - PlotOverlays: each of which is a separate coordinate system.
        Usually, you will only want one PlotOverlay with many plot
        elements in it.  The value of having multiple PlotOverlays is
        that it allows for different coordinate systems described by
        different axes, such as a histogram (PDF) and its accumulation
        (CDF), with the left axis measuring the histogram and the right
        axis measuring the accumulation.
      - Any PLOT-CONTENT-ANNOTAION (PmmlPlotContentAnnotation), which
        is not bound to any in-plot coordinate system.

    Overlays and annotations can appear in any order.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - xlabel: text below the x axis.
      - ylabel: text to the left of the y axis.
      - toplabel: text above the top axis.
      - rightlabel: text to the right of the right axis.
      - colorlabel: text along the color (z) axis (generated by heat maps).
      - xticks-source: 1-based index of the PlotOverlays element to use
        to set the x axis ticks.
      - yticks-source, topticks-source, rightticks-source: same for
        each of the other axes.  This is how one configures a plot with
        two different axes.
      - colorticks-source: same for the color (z) axis.
      - xticks: format specification for the x axis ticks, see PlotTickMarks.
      - yticks, rightticks, topticks: same for the other axes.
      - colorticks: same for the color (z) axis.
      - xticks-draw: one of "nothing", "ticks-only", "parallel-labels",
        "perpendicular-labels": how much of the x axis ticks to draw.
        The "ticks-only" option draws unlabled lines; the "parallel" and
        "perpendicular" options draw text labels that are either aligned
        with or perpendicular to the axis.
      - yticks-draw, topticks-draw, rightticks-draw: same for the other
        axes.
      - colorticks-draw: same for the color (z) ticks.
      - style: CSS style properties.

    CSS properties:

      - margin-top, margin-right, margin-bottom, margin-left,
        margin-colorright, margin: space between the enclosure and the
        border of the plot window, which is very thick on the
        left and bottom to accomodate the tick labels and axis
        labels, which are outside the border.
      - border-top-width, border-right-width, border-bottom-width,
        border-left-width, border-width: thickness of the border.
      - padding-top, padding-right, padding-bottom, padding-left,
        padding: space between the border and the inner content.
      - background, background-opacity: color of the background
        of the plot window.
      - border-color, border-dasharray, border-dashoffset,
        border-linecap, border-linejoin, border-miterlimit,
        border-opacity, border-width: properties of the border line.
      - font, font-family, font-size, font-size-adjust, font-stretch,
        font-style, font-variant, font-weight: font properties for
        the tick labels and axis labels.
      - label-color, xlabel-color, ylabel-color, toplabel-color,
        rightlabel-color, colorlabel-color: label color.
      - ticklabel-color, xticklabel-color, yticklabel-color, topticklabel-color,
        rightticklabel-color, colorticklabel-color: ticklabel color.
      - tick-color, xtick-color, ytick-color, toptick-color,
        righttick-color, colortick-color: tick color.
      - tick-length, xtick-length, ytick-length, toptick-length
        righttick-length, colortick-length: tick length.
      - minitick-length, xminitick-length, yminitick-length,
        topminitick-length, rightminitick-length,
        colorminitick-length: minitick length.
      - xtick-label-xoffset, xtick-label-yoffset, ytick-label-xoffset,
        ytick-label-yoffset, toptick-label-xoffset,
        toptick-label-yoffset, righttick-label-xoffset,
        righttick-label-yoffset, colortick-label-xoffset,
        colortick-label-yoffset: tick label offset.
      - label-margin, xlabel-margin, ylabel-margin, toplabel-margin,
        rightlabel-margin, colorlabel-margin: axis label margin.
      - colorscale-width: width of the colorscale bar.

    See the source code for the full XSD.
    """

    class _State(object):
        pass

    styleProperties = ["margin-top", "margin-right", "margin-bottom", "margin-left", "margin-colorright", "margin",
                       "border-top-width", "border-right-width", "border-bottom-width", "border-left-width", "border-width",
                       "padding-top", "padding-right", "padding-bottom", "padding-left", "padding",
                       "background", "background-opacity", 
                       "border-color", "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width",
                       "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight",
                       "label-color", "xlabel-color", "ylabel-color", "toplabel-color", "rightlabel-color", "colorlabel-color",
                       "ticklabel-color", "xticklabel-color", "yticklabel-color", "topticklabel-color", "rightticklabel-color", "colorticklabel-color",
                       "tick-color", "xtick-color", "ytick-color", "toptick-color", "righttick-color", "colortick-color",
                       "tick-length", "xtick-length", "ytick-length", "toptick-length", "righttick-length", "colortick-length",
                       "minitick-length", "xminitick-length", "yminitick-length", "topminitick-length", "rightminitick-length", "colorminitick-length",
                       "xtick-label-xoffset", "xtick-label-yoffset", "ytick-label-xoffset", "ytick-label-yoffset", "toptick-label-xoffset", "toptick-label-yoffset", "righttick-label-xoffset", "righttick-label-yoffset", "colortick-label-xoffset", "colortick-label-yoffset",
                       "label-margin", "xlabel-margin", "ylabel-margin", "toplabel-margin", "rightlabel-margin", "colorlabel-margin",
                       "colorscale-width"
                       ]

    styleDefaults = {"background": "none", "border-color": "black", "margin": "10", "margin-bottom": "60", "margin-left": "100", "margin-right": "50", "margin-colorright": "50", "padding": "0", "border-width": "2", "font-size": "25.0",
                     "label-color": "black", "ticklabel-color": "black", "tick-color": "black",
                     "tick-length": "20.0", "minitick-length": "10.0",
                     "xtick-label-xoffset": "0.0", "xtick-label-yoffset": "15.0",
                     "ytick-label-xoffset": "-15.0", "ytick-label-yoffset": "0.0",
                     "toptick-label-xoffset": "0.0", "toptick-label-yoffset": "-15.0",
                     "righttick-label-xoffset": "15.0", "righttick-label-yoffset": "0.0",
                     "colortick-label-xoffset": "10.0", "colortick-label-yoffset": "0.0",
                     "label-margin": "17.0",
                     "colorscale-width": "75"
                     }

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotWindow">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:choice minOccurs="0" maxOccurs="unbounded">
                    <xs:element ref="PlotOverlay" minOccurs="1" maxOccurs="1" />
                    <xs:group ref="PLOT-CONTENT-ANNOTATION" minOccurs="1" maxOccurs="1" />
                </xs:choice>
                <xs:sequence minOccurs="0" maxOccurs="1">
                    <xs:element ref="PlotGradientStop" minOccurs="2" maxOccurs="unbounded" />
                </xs:sequence>
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="xlabel" type="xs:string" use="optional" />
            <xs:attribute name="ylabel" type="xs:string" use="optional" />
            <xs:attribute name="toplabel" type="xs:string" use="optional" />
            <xs:attribute name="rightlabel" type="xs:string" use="optional" />
            <xs:attribute name="colorlabel" type="xs:string" use="optional" />
            <xs:attribute name="xticks-source" type="INT-NUMBER" use="optional" default="1" />
            <xs:attribute name="yticks-source" type="INT-NUMBER" use="optional" default="1" />
            <xs:attribute name="topticks-source" type="INT-NUMBER" use="optional" default="1" />
            <xs:attribute name="rightticks-source" type="INT-NUMBER" use="optional" default="1" />
            <xs:attribute name="colorticks-source" type="INT-NUMBER" use="optional" default="1" />
            <xs:attribute name="xticks" type="xs:string" use="optional" default="auto" />
            <xs:attribute name="yticks" type="xs:string" use="optional" default="auto" />
            <xs:attribute name="topticks" type="xs:string" use="optional" default="auto" />
            <xs:attribute name="rightticks" type="xs:string" use="optional" default="auto" />
            <xs:attribute name="colorticks" type="xs:string" use="optional" default="auto" />
            <xs:attribute name="xticks-draw" type="PLOT-TICKS-DRAW" use="optional" default="parallel-labels" />
            <xs:attribute name="yticks-draw" type="PLOT-TICKS-DRAW" use="optional" default="perpendicular-labels" />
            <xs:attribute name="topticks-draw" type="PLOT-TICKS-DRAW" use="optional" default="ticks-only" />
            <xs:attribute name="rightticks-draw" type="PLOT-TICKS-DRAW" use="optional" default="ticks-only" />
            <xs:attribute name="colorticks-draw" type="PLOT-TICKS-DRAW" use="optional" default="perpendicular-labels" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    xsdRemove = ["PLOT-TICKS-DRAW", "UNIT-INTERVAL", "PlotGradientStop"]

    xsdAppend = ["""<xs:simpleType name="PLOT-TICKS-DRAW" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:restriction base="xs:string">
        <xs:enumeration value="nothing" />
        <xs:enumeration value="ticks-only" />
        <xs:enumeration value="parallel-labels" />
        <xs:enumeration value="perpendicular-labels" />
    </xs:restriction>
</xs:simpleType>
""",
                 """<xs:simpleType name="UNIT-INTERVAL" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:restriction base="xs:decimal">
        <xs:minInclusive value="0" />
        <xs:maxInclusive value="1" />
    </xs:restriction>
</xs:simpleType>
""",
                 """<xs:element name="PlotGradientStop" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:complexType>
        <xs:attribute name="offset" type="UNIT-INTERVAL" use="required" />
        <xs:attribute name="red" type="UNIT-INTERVAL" use="required" />
        <xs:attribute name="green" type="UNIT-INTERVAL" use="required" />
        <xs:attribute name="blue" type="UNIT-INTERVAL" use="required" />
        <xs:attribute name="opacity" type="UNIT-INTERVAL" use="optional" default="1" />
    </xs:complexType>
</xs:element>
"""]

    def frame(self, dataTable, functionTable, performanceTable, plotCoordinates, plotContentBox, plotDefinitions):
        """Draw a plot frame and the plot elements it contains.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed (not the coordinate system defined by the plot).
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotWindow")

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        style = self.getStyleState()
        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        adjustForColorScale = []

        ### draw the background

        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            if "background-opacity" in style:
                rectStyle["fill-opacity"] = style["background-opacity"]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {"x": repr(x1), "y": repr(y1), "width": repr(x2 - x1), "height": repr(y2 - y1), "style": PlotStyle.toString(rectStyle)}
            if svgId is not None:
                subAttrib["id"] = svgId + ".background"
            if rectStyle["fill"] != "none":
                r = svg.rect(**subAttrib)
                content.append(r)
                adjustForColorScale.append(r)

        sawAnnotation = False
        aboveTicks = []
        if subContentBox is not None:
            ### create a clipping region for the contents

            if svgId is None:
                svgIdClip = plotDefinitions.uniqueName()
            else:
                svgIdClip = svgId + ".clip"

            r = svg.rect(x=repr(x1), y=repr(y1), width=repr(x2 - x1), height=repr(y2 - y1))
            clipPath = svg.clipPath(r, id=svgIdClip)
            plotDefinitions[svgIdClip] = clipPath
            adjustForColorScale.append(r)

            x1 = subContentBox.x
            y1 = subContentBox.y
            x2 = subContentBox.x + subContentBox.width
            y2 = subContentBox.y + subContentBox.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            clippedDataAttrib = {"clip-path": "url(#%s)" % svgIdClip}

            ### handle the contents

            xticksSourceIndex = self.get("xticks-source", defaultFromXsd=True, convertType=True)
            yticksSourceIndex = self.get("yticks-source", defaultFromXsd=True, convertType=True)
            topticksSourceIndex = self.get("topticks-source", defaultFromXsd=True, convertType=True)
            rightticksSourceIndex = self.get("rightticks-source", defaultFromXsd=True, convertType=True)
            colorticksSourceIndex = self.get("colorticks-source", defaultFromXsd=True, convertType=True)
            xticksSource = None
            yticksSource = None
            topticksSource = None
            rightticksSource = None
            colorticksSource = None

            states = {}
            for coordinatesIndex, overlay in enumerate(self.childrenOfClass(PlotOverlay)):
                plotContents = overlay.childrenOfClass(PmmlPlotContent)

                xlog = overlay.get("xlog", defaultFromXsd=True, convertType=True)
                ylog = overlay.get("ylog", defaultFromXsd=True, convertType=True)
                zlog = overlay.get("zlog", defaultFromXsd=True, convertType=True)
                plotRange = PlotRange(xStrictlyPositive=xlog, yStrictlyPositive=ylog, zStrictlyPositive=zlog)

                ### calculate the contents' coordinates to determine ranges
                performanceTable.pause("PlotWindow")
                for plotContent in plotContents:
                    states[plotContent] = self._State()
                    plotContent.prepare(states[plotContent], dataTable, functionTable, performanceTable, plotRange)
                performanceTable.unpause("PlotWindow")

                xmin, ymin, xmax, ymax = plotRange.ranges()
                xmin = float(overlay.get("xmin", xmin))
                ymin = float(overlay.get("ymin", ymin))
                xmax = float(overlay.get("xmax", xmax))
                ymax = float(overlay.get("ymax", ymax))

                zmin, zmax = plotRange.zranges()

                if zmin is None or zmax is None:
                    zmin = None
                    zmax = None
                else:
                    zmin = float(overlay.get("zmin", zmin))
                    zmax = float(overlay.get("zmax", zmax))

                ### create the inner coordinate system
                plotCoordinatesWindow = PlotCoordinatesWindow(plotCoordinates, xmin, ymin, xmax, ymax, subContentBox.x, subContentBox.y, subContentBox.width, subContentBox.height, flipy=True, xlog=xlog, ylog=ylog, xfieldType=plotRange.xfieldType, yfieldType=plotRange.yfieldType, xstrings=plotRange.xstrings, ystrings=plotRange.ystrings)

                if coordinatesIndex + 1 == xticksSourceIndex:
                    xticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == yticksSourceIndex:
                    yticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == topticksSourceIndex:
                    topticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == rightticksSourceIndex:
                    rightticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == colorticksSourceIndex:
                    colorticksSource = (zmin, zmax, zlog, plotRange.zfieldType)

                for plotContent in plotContents:
                    states[plotContent].plotCoordinatesWindow = plotCoordinatesWindow

            ### figure out if you have any color ticks, since the color tick box shifts the contents
            if colorticksSource is None or zmin is None or zmax is None:
                colorticksSource = None
                colorticksDraw = "nothing"
                colorticks, colorminiticks = None, None
            else:
                zmin, zmax, zlog, cfieldType = colorticksSource

                if zmin is None or zmax is None:
                    colorticksDraw = "nothing"
                    colorticks, colorminiticks = None, None
                else:
                    colorticksDraw = self.get("colorticks-draw", defaultFromXsd=True)
                    tickSpecification = self.get("colorticks", defaultFromXsd=True)

                    if tickSpecification == "auto":
                        if cfieldType.istemporal():
                            colorticks, colorminiticks = PlotTickMarks.interpret("time()", zmin, zmax)
                        elif zlog:
                            colorticks, colorminiticks = PlotTickMarks.interpret("log(~10)", zmin, zmax)
                        else:
                            colorticks, colorminiticks = PlotTickMarks.interpret("linear(~10)", zmin, zmax)
                    elif tickSpecification == "none":
                        colorticks, colorminiticks = {}, []
                    else:
                        colorticks, colorminiticks = PlotTickMarks.interpret(tickSpecification, zmin, zmax)

            gradient = self.childrenOfTag("PlotGradientStop")
            lastStop = None
            for plotGradientStop in gradient:
                offset = float(plotGradientStop["offset"])
                if lastStop is not None and offset <= lastStop:
                    raise defs.PmmlValidationError("Sequence of PlotGradientStop must be strictly increasing in \"offset\"")
                lastStop = offset

            xshiftForColorScale = 0.0
            if colorticksDraw != "nothing":
                xshiftForColorScale += float(style["colorscale-width"]) + float(style["colortick-label-xoffset"]) + float(style["margin-colorright"])
                if self["colorlabel"] is not None:
                    xshiftForColorScale += float(style.get("colorlabel-margin", style["label-margin"]))

            if colorticksSource is not None:
                colorticksSource = PlotCoordinatesWindow(plotCoordinates, 0.0, zmin, 1.0, zmax, subContentBox.x + subContentBox.width - xshiftForColorScale, subContentBox.y, xshiftForColorScale, subContentBox.height, flipy=True, xlog=False, ylog=zlog, xfieldType=cfieldType, yfieldType=cfieldType, xstrings=[], ystrings=[])

                cx2 = plotCoordinates(borderRect.x + borderRect.width, borderRect.y)[0] - float(style["margin-colorright"])
                if self["colorlabel"] is not None:
                    cx2 -= float(style.get("colorlabel-margin", style["label-margin"]))

            for r in adjustForColorScale:
                r["width"] = repr(float(r["width"]) - xshiftForColorScale)

            subContentBox.width -= xshiftForColorScale
            borderRect.width -= xshiftForColorScale

            done = set()
            if xticksSource is not None:
                xticksSource.outerX2 -= xshiftForColorScale
                done.add(xticksSource)
            if yticksSource is not None and yticksSource not in done:
                yticksSource.outerX2 -= xshiftForColorScale
                done.add(yticksSource)
            if topticksSource is not None and topticksSource not in done:
                topticksSource.outerX2 -= xshiftForColorScale
                done.add(topticksSource)
            if rightticksSource is not None and rightticksSource not in done:
                rightticksSource.outerX2 -= xshiftForColorScale
                done.add(rightticksSource)

            ### actually draw the contents and the non-coordinate annotations
            annotationCoordinates = PlotCoordinatesOffset(plotCoordinates, subContentBox.x, subContentBox.y)
            annotationBox = PlotContentBox(0, 0, subContentBox.width, subContentBox.height)

            for overlayOrAnnotation in self.getchildren():
                performanceTable.pause("PlotWindow")

                whatToDraw = []
                if isinstance(overlayOrAnnotation, PlotOverlay):
                    plotContents = overlayOrAnnotation.childrenOfClass(PmmlPlotContent)
                    for plotContent in plotContents:
                        plotCoordinatesWindow = states[plotContent].plotCoordinatesWindow
                        if zmin is not None and zmax is not None:
                            plotCoordinatesWindow.zmin = zmin
                            plotCoordinatesWindow.zmax = zmax
                            plotCoordinatesWindow.zlog = zlog
                            plotCoordinatesWindow.gradient = gradient

                        whatToDraw.append(svg.g(plotContent.draw(states[plotContent], plotCoordinatesWindow, plotDefinitions, performanceTable), **clippedDataAttrib))

                elif isinstance(overlayOrAnnotation, PmmlPlotContentAnnotation):
                    whatToDraw.append(overlayOrAnnotation.draw(dataTable, functionTable, performanceTable, annotationCoordinates, annotationBox, plotDefinitions))
                    sawAnnotation = True

                if sawAnnotation:
                    aboveTicks.extend(whatToDraw)
                else:
                    content.extend(whatToDraw)

                performanceTable.unpause("PlotWindow")

            del states

        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}

            for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                if styleProperty in style:
                    rectStyle[styleProperty.replace("border-", "stroke-")] = style[styleProperty]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {"x": repr(x1), "y": repr(y1), "width": repr(x2 - x1), "height": repr(y2 - y1), "style": PlotStyle.toString(rectStyle)}
            if svgId is not None:
                subAttrib["id"] = svgId + ".border"

            ### draw the tick-marks and axis labels

            leftEdge, topEdge = plotCoordinates(plotContentBox.x, plotContentBox.y)
            rightEdge, bottomEdge = plotCoordinates(plotContentBox.x + plotContentBox.width, plotContentBox.y + plotContentBox.height)

            performanceTable.begin("tickmarks")

            if xticksSource is None:
                xticks = {}
                xminiticks = []
            else:
                tickSpecification = self.get("xticks", defaultFromXsd=True)
                if tickSpecification == "auto":
                    if xticksSource.xfieldType.isstring():
                        xticks, xminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(xticksSource.xstrings)), xticksSource.innerX1, xticksSource.innerX2)
                    elif xticksSource.xfieldType.istemporal():
                        xticks, xminiticks = PlotTickMarks.interpret("time()", xticksSource.innerX1, xticksSource.innerX2)
                    elif xticksSource.xlog:
                        xticks, xminiticks = PlotTickMarks.interpret("log(~10)", xticksSource.innerX1, xticksSource.innerX2)
                    else:
                        xticks, xminiticks = PlotTickMarks.interpret("linear(~10)", xticksSource.innerX1, xticksSource.innerX2)
                elif tickSpecification == "none":
                    xticks, xminiticks = {}, []
                else:
                    xticks, xminiticks = PlotTickMarks.interpret(tickSpecification, xticksSource.innerX1, xticksSource.innerX2)

            if yticksSource is None:
                yticks = {}
                yminiticks = []
            else:
                tickSpecification = self.get("yticks", defaultFromXsd=True)
                if tickSpecification == "auto":
                    if yticksSource.yfieldType.isstring():
                        yticks, yminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(yticksSource.ystrings)), yticksSource.innerY1, yticksSource.innerY2)
                    elif yticksSource.yfieldType.istemporal():
                        yticks, yminiticks = PlotTickMarks.interpret("time()", yticksSource.innerY1, yticksSource.innerY2)
                    elif yticksSource.ylog:
                        yticks, yminiticks = PlotTickMarks.interpret("log(~10)", yticksSource.innerY1, yticksSource.innerY2)
                    else:
                        yticks, yminiticks = PlotTickMarks.interpret("linear(~10)", yticksSource.innerY1, yticksSource.innerY2)
                elif tickSpecification == "none":
                    yticks, yminiticks = {}, []
                else:
                    yticks, yminiticks = PlotTickMarks.interpret(tickSpecification, yticksSource.innerY1, yticksSource.innerY2)

            if topticksSource is None:
                topticks = {}
                topminiticks = []
            else:
                tickSpecification = self.get("topticks", defaultFromXsd=True)
                if tickSpecification == "auto" and topticksSource == xticksSource:
                    topticks, topminiticks = xticks, xminiticks

                elif tickSpecification == "auto":
                    if topticksSource.xfieldType.isstring():
                        topticks, topminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(topticksSource.xstrings)), topticksSource.innerX1, topticksSource.innerX2)
                    elif topticksSource.xfieldType.istemporal():
                        topticks, topminiticks = PlotTickMarks.interpret("time()", topticksSource.innerX1, topticksSource.innerX2)
                    elif topticksSource.xlog:
                        topticks, topminiticks = PlotTickMarks.interpret("log(~10)", topticksSource.innerX1, topticksSource.innerX2)
                    else:
                        topticks, topminiticks = PlotTickMarks.interpret("linear(~10)", topticksSource.innerX1, topticksSource.innerX2)
                    

                elif tickSpecification == "none":
                    topticks, topminiticks = {}, []

                else:
                    topticks, topminiticks = PlotTickMarks.interpret(tickSpecification, topticksSource.innerX1, topticksSource.innerX2)

            if rightticksSource is None:
                rightticks = {}
                rightminiticks = []
            else:
                tickSpecification = self.get("rightticks", defaultFromXsd=True)
                if tickSpecification == "auto" and rightticksSource == yticksSource:
                    rightticks, rightminiticks = yticks, yminiticks

                elif tickSpecification == "auto":
                    if rightticksSource.yfieldType.isstring():
                        rightticks, rightminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(rightticksSource.ystrings)), rightticksSource.innerY1, rightticksSource.innerY2)
                    elif rightticksSource.yfieldType.istemporal():
                        rightticks, rightminiticks = PlotTickMarks.interpret("time()", rightticksSource.innerY1, rightticksSource.innerY2)
                    elif rightticksSource.ylog:
                        rightticks, rightminiticks = PlotTickMarks.interpret("log(~10)", rightticksSource.innerY1, rightticksSource.innerY2)
                    else:
                        rightticks, rightminiticks = PlotTickMarks.interpret("linear(~10)", rightticksSource.innerY1, rightticksSource.innerY2)

                elif tickSpecification == "none":
                    rightticks, rightminiticks = {}, []

                else:
                    rightticks, rightminiticks = PlotTickMarks.interpret(tickSpecification, rightticksSource.innerY1, rightticksSource.innerY2)

            textStyle = {"stroke": "none"}
            for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
                if styleProperty in style:
                    textStyle[styleProperty] = style[styleProperty]

            # very few SVG renderers do dominant-baseline: middle, alignment-baseline: middle, baseline-shift: middle, etc., so we have to emulate it
            dyMiddle = repr(0.35*float(style["font-size"]))

            # x (bottom) ticks
            xticksDraw = self.get("xticks-draw", defaultFromXsd=True)
            if xticksSource is not None and xticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    xticksGroup = svg.g()
                else:
                    xticksGroup = svg.g(id=(svgId + ".xticks"))

                xticklabelColor = style.get("xticklabel-color", style["ticklabel-color"])
                xtickColor = style.get("xtick-color", style["tick-color"])
                xtickLength = float(style.get("xtick-length", style["tick-length"]))
                xminitickLength = float(style.get("xminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (rightEdge - leftEdge)
                transformedTicks = dict((xticksSource(x, 1.0)[0], label) for x, label in xticks.items())
                transformedMiniticks = [xticksSource(x, 1.0)[0] for x in xminiticks if x not in xticks]
                xticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y2, x, y2 - xminitickLength) for x in transformedMiniticks if x1 + eps < x < x2 - eps), style=("stroke: %s; fill: none" % xtickColor)))
                xticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y2, x, y2 - xtickLength) for x in transformedTicks if x1 + eps < x < x2 - eps), style="stroke: %s; fill: none" % xtickColor))

                if xticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["xtick-label-xoffset"]), float(style["xtick-label-yoffset"])
                    textStyle["fill"] = xticklabelColor

                    for x, label in transformedTicks.items():
                        if x1 - eps < x < x2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if xticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x + xoffset, y2 + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif xticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(-90)" % (x + xoffset, y2 + yoffset)
                                labelAttributes["text-anchor"] = "end"
                            xticksGroup.append(svg.text(label, **labelAttributes))

                content.append(xticksGroup)

            # x (bottom) label
            xlabel = self.get("xlabel", "")
            if xlabel != "":
                labelMargin = float(style.get("xlabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("xlabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r)" % ((x1 + x2)/2.0, bottomEdge - labelMargin), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(xlabel, **labelAttributes))

            # y (left) ticks
            yticksDraw = self.get("yticks-draw", defaultFromXsd=True)
            if yticksSource is not None and yticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    yticksGroup = svg.g()
                else:
                    yticksGroup = svg.g(id=(svgId + ".yticks"))

                yticklabelColor = style.get("yticklabel-color", style["ticklabel-color"])
                ytickColor = style.get("ytick-color", style["tick-color"])
                ytickLength = float(style.get("ytick-length", style["tick-length"]))
                yminitickLength = float(style.get("yminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (bottomEdge - topEdge)
                transformedTicks = dict((yticksSource(1.0, y)[1], label) for y, label in yticks.items())
                transformedMiniticks = [yticksSource(1.0, y)[1] for y in yminiticks if y not in yticks]
                yticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x1, y, x1 + yminitickLength, y) for y in transformedMiniticks if y1 + eps < y < y2 - eps), style=("stroke: %s; fill: none" % ytickColor)))
                yticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x1, y, x1 + ytickLength, y) for y in transformedTicks if y1 + eps < y < y2 - eps), style="stroke: %s; fill: none" % ytickColor))

                if yticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["ytick-label-xoffset"]), float(style["ytick-label-yoffset"])
                    textStyle["fill"] = yticklabelColor

                    for y, label in transformedTicks.items():
                        if y1 - eps < y < y2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if yticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(-90)" % (x1 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif yticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x1 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "end"
                            yticksGroup.append(svg.text(label, **labelAttributes))

                content.append(yticksGroup)

            # y (left) label
            ylabel = self.get("ylabel", "")
            if ylabel != "":
                labelMargin = float(style.get("ylabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("ylabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r) rotate(-90)" % (leftEdge + labelMargin, (y1 + y2)/2.0), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(ylabel, **labelAttributes))

            # top ticks
            topticksDraw = self.get("topticks-draw", defaultFromXsd=True)
            if topticksSource is not None and topticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    topticksGroup = svg.g()
                else:
                    topticksGroup = svg.g(id=(svgId + ".topticks"))

                topticklabelColor = style.get("topticklabel-color", style["ticklabel-color"])
                toptickColor = style.get("toptick-color", style["tick-color"])
                toptickLength = float(style.get("toptick-length", style["tick-length"]))
                topminitickLength = float(style.get("topminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (rightEdge - leftEdge)
                transformedTicks = dict((topticksSource(x, 1.0)[0], label) for x, label in topticks.items())
                transformedMiniticks = [topticksSource(x, 1.0)[0] for x in topminiticks if x not in topticks]
                topticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y1, x, y1 + topminitickLength) for x in transformedMiniticks if x1 + eps < x < x2 - eps), style=("stroke: %s; fill: none" % toptickColor)))
                topticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y1, x, y1 + toptickLength) for x in transformedTicks if x1 + eps < x < x2 - eps), style="stroke: %s; fill: none" % toptickColor))

                if topticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["toptick-label-xoffset"]), float(style["toptick-label-yoffset"])
                    textStyle["fill"] = topticklabelColor

                    for x, label in transformedTicks.items():
                        if x1 - eps < x < x2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if topticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x + xoffset, y1 + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif topticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(-90)" % (x + xoffset, y1 + yoffset)
                                labelAttributes["text-anchor"] = "start"
                            topticksGroup.append(svg.text(label, **labelAttributes))

                content.append(topticksGroup)

            # top label
            toplabel = self.get("toplabel", "")
            if toplabel != "":
                labelMargin = float(style.get("toplabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("toplabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r)" % ((x1 + x2)/2.0, topEdge + labelMargin), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(toplabel, **labelAttributes))

            # right ticks
            rightticksDraw = self.get("rightticks-draw", defaultFromXsd=True)
            if rightticksSource is not None and rightticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    rightticksGroup = svg.g()
                else:
                    rightticksGroup = svg.g(id=(svgId + ".rightticks"))

                rightticklabelColor = style.get("rightticklabel-color", style["ticklabel-color"])
                righttickColor = style.get("righttick-color", style["tick-color"])
                righttickLength = float(style.get("righttick-length", style["tick-length"]))
                rightminitickLength = float(style.get("rightminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (bottomEdge - topEdge)
                transformedTicks = dict((rightticksSource(1.0, y)[1], label) for y, label in rightticks.items())
                transformedMiniticks = [rightticksSource(1.0, y)[1] for y in rightminiticks if y not in rightticks]
                rightticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x2, y, x2 - rightminitickLength, y) for y in transformedMiniticks if y1 + eps < y < y2 - eps), style=("stroke: %s; fill: none" % righttickColor)))
                rightticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x2, y, x2 - righttickLength, y) for y in transformedTicks if y1 + eps < y < y2 - eps), style="stroke: %s; fill: none" % righttickColor))

                if rightticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["righttick-label-xoffset"]), float(style["righttick-label-yoffset"])
                    textStyle["fill"] = rightticklabelColor

                    for y, label in transformedTicks.items():
                        if y1 - eps < y < y2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if rightticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(90)" % (x2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif rightticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "start"
                            rightticksGroup.append(svg.text(label, **labelAttributes))

                content.append(rightticksGroup)

            # right label
            rightlabel = self.get("rightlabel", "")
            if rightlabel != "":
                labelMargin = float(style.get("rightlabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("rightlabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r) rotate(90)" % (rightEdge - labelMargin, (y1 + y2)/2.0), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(rightlabel, **labelAttributes))

            # color ticks
            if colorticksSource is not None and colorticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    colorticksGroup = svg.g()
                else:
                    colorticksGroup = svg.g(id=(svgId + ".colorticks"))

                if len(gradient) == 0:
                    linearGradient = svg.linearGradient(id=plotDefinitions.uniqueName(), x1="0%", y1="100%", x2="0%", y2="0%")
                    linearGradient.append(svg.stop(offset="0%", style="stop-color:rgb(255,255,255); stop-opacity: 1.0;"))
                    linearGradient.append(svg.stop(offset="100%", style="stop-color:rgb(0,0,255); stop-opacity: 1.0;"))
                else:
                    linearGradient = svg.linearGradient(id=plotDefinitions.uniqueName(), x1="0%", y1="100%", x2="0%", y2="0%")
                    for stop in gradient:
                        offset = "%r%%" % (100.0 * float(stop["offset"]))
                        gradientStyle = "stop-color:rgb(%r,%r,%r);" % (min(int(math.floor(256.0 * float(stop["red"]))), 255), min(int(math.floor(256.0 * float(stop["green"]))), 255), min(int(math.floor(256.0 * float(stop["blue"]))), 255))
                        opacity = stop.get("opacity")
                        if opacity is not None:
                            gradientStyle += " stop-opacity: %s;" % opacity
                        linearGradient.append(svg.stop(offset=offset, style=gradientStyle))

                plotDefinitions[linearGradient["id"]] = linearGradient

                gradientStyle = rectStyle.copy()
                gradientStyle["fill"] = "url(#%s)" % linearGradient["id"]
                colorticksGroup.append(svg.rect(**{"x": repr(cx2 - float(style["colorscale-width"])), "y": repr(y1), "width": style["colorscale-width"], "height": repr(y2 - y1), "style": PlotStyle.toString(gradientStyle)}))

                colorticklabelColor = style.get("colorticklabel-color", style["ticklabel-color"])
                colortickColor = style.get("colortick-color", style["tick-color"])
                colortickLength = float(style.get("colortick-length", style["tick-length"]))
                colorminitickLength = float(style.get("colorminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (bottomEdge - topEdge)
                transformedTicks = dict((colorticksSource(1.0, y)[1], label) for y, label in colorticks.items())
                transformedMiniticks = [colorticksSource(1.0, y)[1] for y in colorminiticks if y not in colorticks]
                colorticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (cx2, y, cx2 - colorminitickLength, y) for y in transformedMiniticks if y1 + eps < y < y2 - eps), style=("stroke: %s; fill: none" % colortickColor)))
                colorticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (cx2, y, cx2 - colortickLength, y) for y in transformedTicks if y1 + eps < y < y2 - eps), style="stroke: %s; fill: none" % colortickColor))

                if colorticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["colortick-label-xoffset"]), float(style["colortick-label-yoffset"])
                    textStyle["fill"] = colorticklabelColor

                    for y, label in transformedTicks.items():
                        if y1 - eps < y < y2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if colorticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(90)" % (cx2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif colorticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (cx2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "start"
                            colorticksGroup.append(svg.text(label, **labelAttributes))

                content.append(colorticksGroup)

            # color label
            colorlabel = self.get("colorlabel", "")
            if colorlabel != "":
                labelMargin = float(style.get("colorlabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("colorlabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r) rotate(90)" % (rightEdge - labelMargin, (y1 + y2)/2.0), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(colorlabel, **labelAttributes))

            performanceTable.end("tickmarks")

            ### draw the bounding box

            if rectStyle["stroke"] != "none":
                content.append(svg.rect(**subAttrib))

        content.extend(aboveTicks)

        performanceTable.end("PlotWindow")
        return svg.g(*content, **attrib)
Exemple #19
0
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotGuideLines draw")

        output = svg.g()

        for directive in self.xpath("pmml:PlotVerticalLines | pmml:PlotHorizontalLines | pmml:PlotLine"):
            style = dict(self.styleDefaults)
            currentStyle = directive.get("style")
            if currentStyle is not None:
                style.update(PlotStyle.toDict(currentStyle))
            style["fill"] = "none"
            style = PlotStyle.toString(style)

            if directive.hasTag("PlotVerticalLines"):
                try:
                    x0 = plotCoordinates.xfieldType.stringToValue(directive["x0"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid x0: %r" % directive["x0"])

                spacing = float(directive["spacing"])
                low = plotCoordinates.innerX1
                high = plotCoordinates.innerX2

                up = list(NP("arange", x0, high, spacing, dtype=NP.dtype(float)))
                down = list(NP("arange", x0 - spacing, low, -spacing, dtype=NP.dtype(float)))

                for x in up + down:
                    x1, y1 = x, float("-inf")
                    X1, Y1 = plotCoordinates(x1, y1)
                    x2, y2 = x, float("inf")
                    X2, Y2 = plotCoordinates(x2, y2)

                    output.append(svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2), style=style))

            elif directive.hasTag("PlotHorizontalLines"):
                try:
                    y0 = plotCoordinates.xfieldType.stringToValue(directive["y0"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid y0: %r" % directive["y0"])

                spacing = float(directive["spacing"])
                low = plotCoordinates.innerY1
                high = plotCoordinates.innerY2

                up = list(NP("arange", y0, high, spacing, dtype=NP.dtype(float)))
                down = list(NP("arange", y0 - spacing, low, -spacing, dtype=NP.dtype(float)))

                for y in up + down:
                    x1, y1 = float("-inf"), y
                    X1, Y1 = plotCoordinates(x1, y1)
                    x2, y2 = float("inf"), y
                    X2, Y2 = plotCoordinates(x2, y2)

                    output.append(svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2), style=style))

            elif directive.hasTag("PlotLine"):
                try:
                    x1 = plotCoordinates.xfieldType.stringToValue(directive["x1"])
                    y1 = plotCoordinates.xfieldType.stringToValue(directive["y1"])
                    x2 = plotCoordinates.xfieldType.stringToValue(directive["x2"])
                    y2 = plotCoordinates.xfieldType.stringToValue(directive["y2"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid x1, y1, x2, or y2: %r %r %r %r" % (directive["x1"], directive["y1"], directive["x2"], directive["y2"]))

                X1, Y1 = plotCoordinates(x1, y1)
                X2, Y2 = plotCoordinates(x2, y2)

                output.append(svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2), style=style))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotGuideLines draw")

        return output
Exemple #20
0
    def makeMarker(svgIdMarker, marker, style, plotSvgMarker):
        """Construct a marker from a set of known shapes or an SVG
        pictogram.

        @type svgIdMarker: string
        @param svgIdMarker: SVG id for the new marker.
        @type marker: string
        @param marker: Name of the marker shape; must be one of PLOT-MARKER-TYPE.
        @type style: dict
        @param style: CSS style for the marker in dictionary form.
        @type plotSvgMarker: PmmlBinding or None
        @param plotSvgMarker: A PlotSvgMarker element, which either contains an inline SvgBinding or a fileName pointing to an external image.
        @rtype: SvgBinding
        @return: The marker image, appropriate for adding to a PlotDefinitions.
        """

        svg = SvgBinding.elementMaker

        style["stroke"] = style["marker-outline"]
        del style["marker-outline"]
        markerSize = float(style["marker-size"])
        del style["marker-size"]

        if marker == "circle":
            return svg.circle(id=svgIdMarker, cx="0", cy="0", r=repr(markerSize), style=PlotStyle.toString(style))

        elif marker == "square":
            p =  markerSize
            m = -markerSize
            return svg.path(id=svgIdMarker, d="M %r,%r L %r,%r L %r,%r L %r,%r z" % (m,m, p,m, p,p, m,p), style=PlotStyle.toString(style))

        elif marker == "diamond":
            p =  math.sqrt(2.0) * markerSize
            m = -math.sqrt(2.0) * markerSize
            return svg.path(id=svgIdMarker, d="M %r,0 L 0,%r L %r,0 L 0,%r z" % (m, m, p, p), style=PlotStyle.toString(style))

        elif marker == "plus":
            p =  markerSize
            m = -markerSize
            if style["stroke"] == "none":
                style["stroke"] = style["fill"]
            style["fill"] = "none"
            return svg.path(id=svgIdMarker, d="M %r,0 L %r,0 M 0,%r L 0,%r" % (m, p, m, p), style=PlotStyle.toString(style))

        elif marker == "times":
            p =  math.sqrt(2.0) * markerSize
            m = -math.sqrt(2.0) * markerSize
            if style["stroke"] == "none":
                style["stroke"] = style["fill"]
            style["fill"] = "none"
            return svg.path(id=svgIdMarker, d="M %r,%r L %r,%r M %r,%r L %r,%r" % (m,m, p,p, p,m, m,p), style=PlotStyle.toString(style))

        elif marker == "svg":
            if plotSvgMarker is None:
                raise defs.PmmlValidationError("When marker is \"svg\", a PlotSvgMarker must be provided")

            inlineSvg = plotSvgMarker.getchildren()
            fileName = plotSvgMarker.get("fileName")
            if len(inlineSvg) == 1 and fileName is None:
                svgBinding = inlineSvg[0]
            elif len(inlineSvg) == 0 and fileName is not None:
                svgBinding = SvgBinding.loadXml(fileName)
            else:
                raise defs.PmmlValidationError("PlotSvgMarker should specify an inline SVG or a fileName but not both or neither")

            sx1, sy1, sx2, sy2 = PlotSvgAnnotation.findSize(svgBinding)
            tx1, ty1 = -markerSize, -markerSize
            tx2, ty2 = markerSize, markerSize

            transform = "translate(%r, %r) scale(%r, %r)" % (tx1 - sx1, ty1 - sy1, (tx2 - tx1)/float(sx2 - sx1), (ty2 - ty1)/float(sy2 - sy1))
            return svg.g(copy.deepcopy(svgBinding), id=svgIdMarker, transform=transform)
Exemple #21
0
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotHistogram draw")

        cumulative = self.get("cumulative",
                              defaultFromXsd=True,
                              convertType=True)
        vertical = self.get("vertical", defaultFromXsd=True, convertType=True)
        visualization = self.get("visualization", defaultFromXsd=True)

        output = svg.g()
        if len(state.count) > 0:
            if state.fieldType is not self.fieldTypeNumeric:
                if vertical:
                    strings = plotCoordinates.xstrings
                else:
                    strings = plotCoordinates.ystrings

                newCount = []
                for string in strings:
                    try:
                        index = state.edges.index(string)
                    except ValueError:
                        newCount.append(0.0)
                    else:
                        newCount.append(state.count[index])

                state.count = newCount
                state.edges = [(i - 0.5, i + 0.5)
                               for i in xrange(len(strings))]

            if vertical:
                Ax = NP("array", [
                    low if low is not None else float("-inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                Bx = NP(Ax.copy())
                Cx = NP("array", [
                    high if high is not None else float("inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                Dx = NP(Cx.copy())
                Ay = NP("zeros", len(state.count), dtype=NP.dtype(float))
                if cumulative:
                    Cy = NP("cumsum",
                            NP("array", state.count, dtype=NP.dtype(float)))
                    By = NP("roll", Cy, 1)
                    By[0] = 0.0
                else:
                    By = NP("array", state.count, dtype=NP.dtype(float))
                    Cy = NP(By.copy())
                Dy = NP(Ay.copy())

            else:
                if cumulative:
                    Cx = NP("cumsum",
                            NP("array", state.count, dtype=NP.dtype(float)))
                    Bx = NP("roll", Cx, 1)
                    Bx[0] = 0.0
                else:
                    Bx = NP("array", state.count, dtype=NP.dtype(float))
                    Cx = NP(Bx.copy())
                Ax = NP("zeros", len(state.count), dtype=NP.dtype(float))
                Dx = NP(Ax.copy())
                Ay = NP("array", [
                    low if low is not None else float("-inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                By = NP(Ay.copy())
                Cy = NP("array", [
                    high if high is not None else float("inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                Dy = NP(Cy.copy())

            AX, AY = plotCoordinates(Ax, Ay)
            BX, BY = plotCoordinates(Bx, By)
            CX, CY = plotCoordinates(Cx, Cy)
            DX, DY = plotCoordinates(Dx, Dy)

            if visualization == "skyline":
                gap = self.get("gap", defaultFromXsd=True, convertType=True)

                if vertical:
                    if gap > 0.0 and NP(
                            NP(DX - gap / 2.0) -
                            NP(AX + gap / 2.0)).min() > 0.0:
                        AX += gap / 2.0
                        BX += gap / 2.0
                        CX -= gap / 2.0
                        DX -= gap / 2.0
                else:
                    if gap > 0.0 and NP(
                            NP(AY + gap / 2.0) -
                            NP(DY - gap / 2.0)).min() > 0.0:
                        AY -= gap / 2.0
                        BY -= gap / 2.0
                        CY += gap / 2.0
                        DY += gap / 2.0

                pathdata = []
                nextIsMoveto = True
                for i in xrange(len(state.count)):
                    iprev = i - 1
                    inext = i + 1

                    if vertical and By[i] == 0.0 and Cy[i] == 0.0:
                        if i > 0 and not nextIsMoveto:
                            pathdata.append("L %r %r" % (DX[iprev], DY[iprev]))
                        nextIsMoveto = True

                    elif not vertical and Bx[i] == 0.0 and Cx[i] == 0.0:
                        if i > 0 and not nextIsMoveto:
                            pathdata.append("L %r %r" % (DX[iprev], DY[iprev]))
                        nextIsMoveto = True

                    else:
                        if nextIsMoveto or gap > 0.0 or (
                                vertical and DX[iprev] != AX[i]) or (
                                    not vertical and DY[iprev] != AY[i]):
                            pathdata.append("M %r %r" % (AX[i], AY[i]))
                            nextIsMoveto = False

                        pathdata.append("L %r %r" % (BX[i], BY[i]))
                        pathdata.append("L %r %r" % (CX[i], CY[i]))

                        if i == len(state.count) - 1 or gap > 0.0 or (
                                vertical and DX[i] != AX[inext]) or (
                                    not vertical and DY[i] != AY[inext]):
                            pathdata.append("L %r %r" % (DX[i], DY[i]))

                style = self.getStyleState()
                del style["marker-size"]
                del style["marker-outline"]
                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(style)))

            elif visualization == "polyline":
                pathdata = []
                for i in xrange(len(state.count)):
                    if i == 0:
                        pathdata.append("M %r %r" % (AX[i], AY[i]))

                    pathdata.append("L %r %r" % ((BX[i] + CX[i]) / 2.0,
                                                 (BY[i] + CY[i]) / 2.0))

                    if i == len(state.count) - 1:
                        pathdata.append("L %r %r" % (DX[i], DY[i]))

                style = self.getStyleState()
                del style["marker-size"]
                del style["marker-outline"]
                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(style)))

            elif visualization == "smooth":
                smoothingSamples = math.ceil(len(state.count) / 2.0)

                BCX = NP(NP(BX + CX) / 2.0)
                BCY = NP(NP(BY + CY) / 2.0)

                xarray = NP("array", [AX[0]] + list(BCX) + [DX[-1]],
                            dtype=NP.dtype(float))
                yarray = NP("array", [AY[0]] + list(BCY) + [DY[-1]],
                            dtype=NP.dtype(float))
                samples = NP("linspace",
                             AX[0],
                             DX[-1],
                             int(smoothingSamples),
                             endpoint=True)
                smoothingScale = abs(DX[-1] - AX[0]) / smoothingSamples

                xlist, ylist, dxlist, dylist = PlotCurve.pointsToSmoothCurve(
                    xarray, yarray, samples, smoothingScale, False)

                pathdata = PlotCurve.formatPathdata(xlist, ylist,
                                                    dxlist, dylist,
                                                    PlotCoordinates(), False,
                                                    True)

                style = self.getStyleState()
                fillStyle = dict(
                    (x, style[x]) for x in style if x.startswith("fill"))
                fillStyle["stroke"] = "none"
                strokeStyle = dict(
                    (x, style[x]) for x in style if x.startswith("stroke"))

                if style["fill"] != "none" and len(pathdata) > 0:
                    if vertical:
                        firstPoint = plotCoordinates(Ax[0], 0.0)
                        lastPoint = plotCoordinates(Dx[-1], 0.0)
                    else:
                        firstPoint = plotCoordinates(0.0, Ay[0])
                        lastPoint = plotCoordinates(0.0, Dy[-1])

                    pathdata2 = [
                        "M %r %r" % firstPoint, pathdata[0].replace("M", "L")
                    ]
                    pathdata2.extend(pathdata[1:])
                    pathdata2.append(pathdata[-1])
                    pathdata2.append("L %r %r" % lastPoint)

                    output.append(
                        svg.path(d=" ".join(pathdata2),
                                 style=PlotStyle.toString(fillStyle)))

                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(strokeStyle)))

            elif visualization == "points":
                currentStyle = PlotStyle.toDict(self.get("style") or {})
                style = self.getStyleState()
                if "fill" not in currentStyle:
                    style["fill"] = "black"

                BCX = NP(NP(BX + CX) / 2.0)
                BCY = NP(NP(BY + CY) / 2.0)

                svgId = self.get("svgId")
                if svgId is None:
                    svgIdMarker = plotDefinitions.uniqueName()
                else:
                    svgIdMarker = svgId + ".marker"

                marker = PlotScatter.makeMarker(
                    svgIdMarker, self.get("marker", defaultFromXsd=True),
                    style, self.childOfTag("PlotSvgMarker"))
                plotDefinitions[marker.get("id")] = marker

                markerReference = "#" + marker.get("id")
                output.extend(
                    svg.use(
                        **{
                            "x": repr(x),
                            "y": repr(y),
                            defs.XLINK_HREF: markerReference
                        }) for x, y in itertools.izip(BCX, BCY))

            else:
                raise NotImplementedError("TODO: add 'errorbars'")

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotHistogram draw")
        return output
Exemple #22
0
class PlotHistogram(PmmlPlotContent):
    """Represents a 1d histogram of the data.

    PMML subelements:

      - PlotExpression role="data": the numeric or categorical data.
      - PlotNumericExpression role="weight": histogram weights.
      - PlotSelection: expression or predicate to filter the data
        before plotting.
      - Intervals: non-uniform (numerical) histogram bins.
      - Values: explicit (categorical) histogram values.
      - PlotSvgMarker: inline SVG for histograms drawn with markers,
        where the markers are SVG pictograms.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - stateId: key for persistent storage in a DataTableState.
      - numBins: number of histogram bins.
      - low: histogram low edge.
      - high: histogram high edge.
      - normalized: if "false", the histogram represents the number
        of counts in each bin; if "true", the histogram represents
        density, with a total integral (taking into account bin
        widths) of 1.0.
      - cumulative: if "false", the histogram approximates a
        probability density function (PDF) with flat-top bins;
        if "true", the histogram approximates a cumulative
        distribution function (CDF) with linear-top bins.
      - vertical: if "true", plot the "data" expression on the x
        axis and the counts/density/cumulative values on the y
        axis.
      - visualization: one of "skyline", "polyline", "smooth",
        "points", "errorbars".
      - gap: size of the space between histogram bars in SVG
        coordinates.
      - marker: marker to use for "points" visualization (see
        PlotScatter).
      - style: CSS style properties.
        
    CSS properties:
      - fill, fill-opacity: color of the histogram bars.
      - stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap,
        stroke-linejoin, stroke-miterlimit, stroke-opacity,
        stroke-width: properties of the line drawing.
      - marker-size, marker-outline: marker style for "points"
        visualization.

    See the source code for the full XSD.
    """

    styleProperties = [
        "fill",
        "fill-opacity",
        "stroke",
        "stroke-dasharray",
        "stroke-dashoffset",
        "stroke-linecap",
        "stroke-linejoin",
        "stroke-miterlimit",
        "stroke-opacity",
        "stroke-width",
        "marker-size",
        "marker-outline",
    ]

    styleDefaults = {
        "fill": "none",
        "stroke": "black",
        "marker-size": "5",
        "marker-outline": "none"
    }

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotHistogram">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:element ref="PlotExpression" minOccurs="1" maxOccurs="1" />
                <xs:element ref="PlotNumericExpression" minOccurs="0" maxOccurs="1" />
                <xs:element ref="PlotSelection" minOccurs="0" maxOccurs="1" />
                <xs:choice minOccurs="0" maxOccurs="1">
                    <xs:element ref="Interval" minOccurs="1" maxOccurs="unbounded" />
                    <xs:element ref="Value" minOccurs="1" maxOccurs="unbounded" />
                </xs:choice>
                <xs:element ref="PlotSvgMarker" minOccurs="0" maxOccurs="1" />
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="stateId" type="xs:string" use="optional" />
            <xs:attribute name="numBins" type="xs:positiveInteger" use="optional" />
            <xs:attribute name="low" type="xs:double" use="optional" />
            <xs:attribute name="high" type="xs:double" use="optional" />
            <xs:attribute name="normalized" type="xs:boolean" use="optional" default="false" />
            <xs:attribute name="cumulative" type="xs:boolean" use="optional" default="false" />
            <xs:attribute name="vertical" type="xs:boolean" use="optional" default="true" />
            <xs:attribute name="visualization" use="optional" default="skyline">
                <xs:simpleType>
                    <xs:restriction base="xs:string">
                        <xs:enumeration value="skyline" />
                        <xs:enumeration value="polyline" />
                        <xs:enumeration value="smooth" />
                        <xs:enumeration value="points" />
                        <xs:enumeration value="errorbars" />
                    </xs:restriction>
                </xs:simpleType>
            </xs:attribute>
            <xs:attribute name="gap" type="xs:double" use="optional" default="0.0" />
            <xs:attribute name="marker" type="PLOT-MARKER-TYPE" use="optional" default="circle" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    fieldType = FakeFieldType("double", "continuous")
    fieldTypeNumeric = FakeFieldType("double", "continuous")

    @staticmethod
    def establishBinType(fieldType, intervals, values):
        """Determine the type of binning to use for a histogram with
        the given FieldType, Intervals, and Values.

        @type fieldType: FieldType
        @param fieldType: The FieldType of the plot expression.
        @type intervals: list of PmmlBinding
        @param intervals: The <Interval> elements; may be empty.
        @type values: list of PmmlBinding
        @param values: The <Value> elements; may be empty.
        @rtype: string
        @return: One of "nonuniform", "explicit", "unique", "scale".
        """

        if len(intervals) > 0:
            if not fieldType.isnumeric() and not fieldType.istemporal():
                raise defs.PmmlValidationError(
                    "Explicit Intervals are intended for numerical data, not %r"
                    % fieldType)
            return "nonuniform"

        elif len(values) > 0:
            if not fieldType.isstring():
                raise defs.PmmlValidationError(
                    "Explicit Values are intended for string data, not %r" %
                    fieldType)
            return "explicit"

        elif fieldType.isstring():
            return "unique"

        else:
            if not fieldType.isnumeric() and not fieldType.istemporal():
                raise defs.PmmlValidationError(
                    "PlotHistogram requires numerical or string data, not %r" %
                    fieldType)
            return "scale"

    @staticmethod
    def determineScaleBins(numBins, low, high, array):
        """Determine the C{numBins}, C{low}, and C{high} of the
        histogram from explicitly set values where available and
        implicitly derived values where necessary.

        Explicitly set values always override implicit values derived
        from the dataset.
          - C{low}, C{high} implicit values are the extrema of the
            dataset.
          - C{numBins} implicit value is the Freedman-Diaconis
            heuristic for number of histogram bins.

        @type numBins: int or None
        @param numBins: Input number of bins.
        @type low: number or None
        @param low: Low edge.
        @type high: number or None
        @param high: High edge.
        @type array: 1d Numpy array of numbers
        @param array: Dataset to use to implicitly derive values.
        @rtype: 3-tuple
        @return: C{numBins}, C{low}, C{high}
        """

        generateLow = (low is None)
        generateHigh = (high is None)

        if generateLow: low = float(array.min())
        if generateHigh: high = float(array.max())

        if low == high:
            low, high = low - 1.0, high + 1.0
        elif high < low:
            if generateLow:
                low = high - 1.0
            elif generateHigh:
                high = low + 1.0
            else:
                raise defs.PmmlValidationError(
                    "PlotHistogram attributes low and high must be in the right order: low = %g, high = %g"
                    % (low, high))
        else:
            if generateLow and generateHigh:
                low, high = low - 0.2 * (high - low), high + 0.2 * (high - low)
            elif generateLow:
                low = low - 0.2 * (high - low)
            elif generateHigh:
                high = high + 0.2 * (high - low)

        if numBins is None:
            # the Freedman-Diaconis rule
            q1, q3 = NP("percentile", array, [25.0, 75.0])
            binWidth = 2.0 * (q3 - q1) / math.pow(len(array), 1.0 / 3.0)
            if binWidth > 0.0:
                numBins = max(10, int(math.ceil((high - low) / binWidth)))
            else:
                numBins = 10

        return numBins, low, high

    @staticmethod
    def selectInterval(fieldType, array, index, lastIndex, interval, edges,
                       lastLimitPoint, lastClosed, lastInterval):
        """Select rows of an array within an interval as part of
        filling a non-uniform histogram.

        @type fieldType: FieldType
        @param fieldType: FieldType used to interpret the bounds of the interval.
        @type array: 1d Numpy array
        @param array: Values to select.
        @type index: int
        @param index: Current bin index.
        @type lastIndex: int
        @param lastIndex: Previous bin index.
        @type interval: PmmlBinding
        @param interval: PMML <Interval> element defining the interval.
        @type edges: list of 2-tuples
        @param edges: Pairs of interpreted C{leftMargin}, C{rightMargin} for the histogram.
        @type lastLimitPoint: number
        @param lastLimitPoint: Larger of the two last edges.  ("Limit point" because it may have been open or closed.)
        @type lastClosed: bool
        @param lastClosed: If True, the last limit point was closed.
        @type lastInterval: PmmlBinding
        @param lastInterval: PMML <Interval> for the last bin.
        @rtype: 4-tuple
        @return: C{selection} (1d Numpy array of bool), C{lastLimitPoint}, C{lastClosed}, C{lastInterval}
        """

        closure = interval["closure"]
        leftMargin = interval.get("leftMargin")
        rightMargin = interval.get("rightMargin")

        selection = None

        if leftMargin is None and rightMargin is None and len(intervals) != 1:
            raise defs.PmmlValidationError(
                "If a histogram bin is unbounded on both ends, it must be the only bin"
            )

        if leftMargin is not None:
            try:
                leftMargin = fieldType.stringToValue(leftMargin)
            except ValueError:
                raise defs.PmmlValidationError(
                    "Improper value in Interval leftMargin specification: \"%s\""
                    % leftMargin)

            if closure in ("openClosed", "openOpen"):
                if selection is None:
                    selection = NP(leftMargin < array)
                else:
                    NP("logical_and", selection, NP(leftMargin < array),
                       selection)

            elif closure in ("closedOpen", "closedClosed"):
                if selection is None:
                    selection = NP(leftMargin <= array)
                else:
                    NP("logical_and", selection, NP(leftMargin <= array),
                       selection)

            if lastLimitPoint is not None:
                if leftMargin < lastLimitPoint or (
                        leftMargin == lastLimitPoint and
                    (closure in ("closedOpen", "closedClosed"))
                        and lastClosed):
                    raise defs.PmmlValidationError(
                        "Intervals are out of order or overlap: %r and %r" %
                        (lastInterval, interval))

        elif index != 0:
            raise defs.PmmlValidationError(
                "Only the first Interval can have an open-ended leftMargin: %r"
                % interval)

        if rightMargin is not None:
            try:
                rightMargin = fieldType.stringToValue(rightMargin)
            except ValueError:
                raise defs.PmmlValidationError(
                    "Improper value in Interval rightMargin specification: \"%s\""
                    % rightMargin)

            if closure in ("openOpen", "closedOpen"):
                if selection is None:
                    selection = NP(array < rightMargin)
                else:
                    NP("logical_and", selection, NP(array < rightMargin),
                       selection)

            elif closure in ("openClosed", "closedClosed"):
                if selection is None:
                    selection = NP(array <= rightMargin)
                else:
                    NP("logical_and", selection, NP(array <= rightMargin),
                       selection)

            lastLimitPoint = rightMargin
            lastClosed = (closure in ("openClosed", "closedClosed"))
            lastInterval = interval

        elif index != lastIndex:
            raise defs.PmmlValidationError(
                "Only the last Interval can have an open-ended rightMargin: %r"
                % interval)

        edges.append((leftMargin, rightMargin))

        return selection, lastLimitPoint, lastClosed, lastInterval

    def prepare(self, state, dataTable, functionTable, performanceTable,
                plotRange):
        """Prepare a plot element for drawing.

        This stage consists of calculating all quantities and
        determing the bounds of the data.  These bounds may be unioned
        with bounds from other plot elements that overlay this plot
        element, so the drawing (which requires a finalized coordinate
        system) cannot begin yet.

        This method modifies C{plotRange}.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotRange: PlotRange
        @param plotRange: The bounding box of plot coordinates that this function will expand.
        """

        self.checkRoles(["data", "weight"])

        dataExpression = self.xpath("pmml:PlotExpression[@role='data']")
        weightExpression = self.xpath(
            "pmml:PlotNumericExpression[@role='weight']")
        cutExpression = self.xpath("pmml:PlotSelection")
        if len(dataExpression) != 1:
            raise defs.PmmlValidationError(
                "PlotHistogram requires a PlotNumericExpression with role \"data\""
            )

        dataColumn = dataExpression[0].evaluate(dataTable, functionTable,
                                                performanceTable)

        if len(weightExpression) == 0:
            weight = None
        elif len(weightExpression) == 1:
            weight = weightExpression[0].evaluate(dataTable, functionTable,
                                                  performanceTable)
        else:
            raise defs.PmmlValidationError(
                "PlotHistogram may not have more than one PlotNumericExpression with role \"data\""
            )

        if len(cutExpression) == 1:
            selection = cutExpression[0].select(dataTable, functionTable,
                                                performanceTable)
        else:
            selection = NP("ones", len(dataTable), NP.dtype(bool))

        performanceTable.begin("PlotHistogram prepare")
        self._saveContext(dataTable)

        if dataColumn.mask is not None:
            NP("logical_and", selection, NP(dataColumn.mask == defs.VALID),
               selection)

        if weight is not None and weight.mask is not None:
            NP("logical_and", selection, NP(weight.mask == defs.VALID),
               selection)

        array = dataColumn.data[selection]
        if weight is not None:
            weight = weight.data[selection]

        persistentState = {}
        stateId = self.get("stateId")
        if stateId is not None:
            if stateId in dataTable.state:
                persistentState = dataTable.state[stateId]
            else:
                dataTable.state[stateId] = persistentState

        intervals = self.xpath("pmml:Interval")
        values = self.xpath("pmml:Value")

        if "binType" not in persistentState:
            performanceTable.begin("establish binType")

            binType = self.establishBinType(dataColumn.fieldType, intervals,
                                            values)
            persistentState["binType"] = binType

            if binType == "nonuniform":
                persistentState["count"] = [0.0] * len(intervals)

            elif binType == "explicit":
                persistentState["count"] = [0.0] * len(values)

            elif binType == "unique":
                persistentState["count"] = {}

            elif binType == "scale":
                numBins = self.get("numBins", convertType=True)
                low = self.get("low", convertType=True)
                high = self.get("high", convertType=True)

                numBins, low, high = self.determineScaleBins(
                    numBins, low, high, array)

                persistentState["low"] = low
                persistentState["high"] = high
                persistentState["numBins"] = numBins
                persistentState["count"] = [0.0] * numBins

            performanceTable.end("establish binType")

        missingSum = 0.0
        if persistentState["binType"] == "nonuniform":
            performanceTable.begin("binType nonuniform")

            count = [0.0] * len(intervals)
            edges = []
            lastLimitPoint = None
            lastClosed = None
            lastInterval = None

            for index, interval in enumerate(intervals):
                selection, lastLimitPoint, lastClosed, lastInterval = self.selectInterval(
                    dataColumn.fieldType, array, index,
                    len(intervals) - 1, interval, edges, lastLimitPoint,
                    lastClosed, lastInterval)

                if selection is not None:
                    if weight is None:
                        count[index] += NP("count_nonzero", selection)
                    else:
                        count[index] += weight[selection].sum()

            persistentState["count"] = [
                x + y
                for x, y in itertools.izip(count, persistentState["count"])
            ]

            state.fieldType = self.fieldTypeNumeric
            state.count = persistentState["count"]
            state.edges = edges
            lowEdge = min(low for low, high in edges if low is not None)
            highEdge = max(high for low, high in edges if high is not None)

            performanceTable.end("binType nonuniform")

        elif persistentState["binType"] == "explicit":
            performanceTable.begin("binType explicit")

            count = [0.0] * len(values)
            displayValues = []

            for index, value in enumerate(values):
                internalValue = dataColumn.fieldType.stringToValue(
                    value["value"])
                displayValues.append(
                    value.get(
                        "displayValue",
                        dataColumn.fieldType.valueToString(internalValue,
                                                           displayValue=True)))

                selection = NP(array == internalValue)

                if weight is None:
                    count[index] += NP("count_nonzero", selection)
                else:
                    count[index] += weight[selection].sum()

            persistentState["count"] = [
                x + y
                for x, y in itertools.izip(count, persistentState["count"])
            ]

            state.fieldType = dataColumn.fieldType
            state.count = persistentState["count"]
            state.edges = displayValues

            performanceTable.end("binType explicit")

        elif persistentState["binType"] == "unique":
            performanceTable.begin("binType unique")

            uniques, inverse = NP("unique", array, return_inverse=True)
            if weight is None:
                counts = NP("bincount", inverse)
            else:
                counts = NP("bincount", inverse, weights=weight)

            persistentCount = persistentState["count"]
            for i, u in enumerate(uniques):
                string = dataColumn.fieldType.valueToString(u,
                                                            displayValue=False)

                if string in persistentCount:
                    persistentCount[string] += counts[i]
                else:
                    persistentCount[string] = counts[i]

            tosort = [(count, string)
                      for string, count in persistentCount.items()]
            tosort.sort(reverse=True)

            numBins = self.get("numBins", convertType=True)
            if numBins is not None:
                missingSum = sum(count for count, string in tosort[numBins:])
                tosort = tosort[:numBins]

            state.fieldType = dataColumn.fieldType
            state.count = [count for count, string in tosort]
            state.edges = [
                dataColumn.fieldType.valueToString(
                    dataColumn.fieldType.stringToValue(string),
                    displayValue=True) for count, string in tosort
            ]

            performanceTable.end("binType unique")

        elif persistentState["binType"] == "scale":
            performanceTable.begin("binType scale")

            numBins = persistentState["numBins"]
            low = persistentState["low"]
            high = persistentState["high"]
            binWidth = (high - low) / float(numBins)

            binAssignments = NP("array",
                                NP("floor", NP(NP(array - low) / binWidth)),
                                dtype=NP.dtype(int))
            binAssignments[NP(binAssignments > numBins)] = numBins
            binAssignments[NP(binAssignments < 0)] = numBins

            if len(binAssignments) == 0:
                count = NP("empty", 0, dtype=NP.dtype(float))
            else:
                if weight is None:
                    count = NP("bincount", binAssignments)
                else:
                    count = NP("bincount", binAssignments, weights=weight)

            if len(count) < numBins:
                padded = NP("zeros", numBins, dtype=NP.dtype(float))
                padded[:len(count)] = count
            else:
                padded = count

            persistentState["count"] = [
                x + y
                for x, y in itertools.izip(padded, persistentState["count"])
            ]

            state.fieldType = self.fieldTypeNumeric
            state.count = persistentState["count"]
            state.edges = [(low + i * binWidth, low + (i + 1) * binWidth)
                           for i in xrange(numBins)]
            lowEdge = low
            highEdge = high

            performanceTable.end("binType scale")

        if self.get("normalized", defaultFromXsd=True, convertType=True):
            if state.fieldType is self.fieldTypeNumeric:
                weightedValues = 0.0
                for (low,
                     high), value in itertools.izip(state.edges, state.count):
                    if low is not None and high is not None:
                        weightedValues += value / (high - low)

                newCount = []
                for (low, high), value in zip(state.edges, state.count):
                    if low is None or high is None:
                        newCount.append(0.0)
                    else:
                        newCount.append(value / (high - low) / weightedValues)

                state.count = newCount

            else:
                totalCount = sum(state.count) + missingSum
                state.count = [float(x) / totalCount for x in state.count]

        if self.get("cumulative", defaultFromXsd=True, convertType=True):
            maximum = sum(state.count)
        else:
            maximum = max(state.count)

        if self.get("vertical", defaultFromXsd=True, convertType=True):
            plotRange.yminPush(0.0, self.fieldType, sticky=True)

            if state.fieldType is self.fieldTypeNumeric:
                plotRange.xminPush(lowEdge, state.fieldType, sticky=True)
                plotRange.xmaxPush(highEdge, state.fieldType, sticky=True)
                plotRange.ymaxPush(maximum, state.fieldType, sticky=False)
            else:
                plotRange.expand(
                    NP("array", state.edges, dtype=NP.dtype(object)),
                    NP("ones", len(state.edges), dtype=NP.dtype(float)) *
                    maximum, state.fieldType, self.fieldType)

        else:
            plotRange.xminPush(0.0, self.fieldType, sticky=True)

            if state.fieldType is self.fieldTypeNumeric:
                plotRange.yminPush(lowEdge, state.fieldType, sticky=True)
                plotRange.ymaxPush(highEdge, state.fieldType, sticky=True)
                plotRange.xmaxPush(maximum, state.fieldType, sticky=False)
            else:
                plotRange.expand(
                    NP("ones", len(state.edges), dtype=NP.dtype(float)) *
                    maximum, NP("array", state.edges, dtype=NP.dtype(object)),
                    self.fieldType, state.fieldType)

        performanceTable.end("PlotHistogram prepare")

    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotHistogram draw")

        cumulative = self.get("cumulative",
                              defaultFromXsd=True,
                              convertType=True)
        vertical = self.get("vertical", defaultFromXsd=True, convertType=True)
        visualization = self.get("visualization", defaultFromXsd=True)

        output = svg.g()
        if len(state.count) > 0:
            if state.fieldType is not self.fieldTypeNumeric:
                if vertical:
                    strings = plotCoordinates.xstrings
                else:
                    strings = plotCoordinates.ystrings

                newCount = []
                for string in strings:
                    try:
                        index = state.edges.index(string)
                    except ValueError:
                        newCount.append(0.0)
                    else:
                        newCount.append(state.count[index])

                state.count = newCount
                state.edges = [(i - 0.5, i + 0.5)
                               for i in xrange(len(strings))]

            if vertical:
                Ax = NP("array", [
                    low if low is not None else float("-inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                Bx = NP(Ax.copy())
                Cx = NP("array", [
                    high if high is not None else float("inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                Dx = NP(Cx.copy())
                Ay = NP("zeros", len(state.count), dtype=NP.dtype(float))
                if cumulative:
                    Cy = NP("cumsum",
                            NP("array", state.count, dtype=NP.dtype(float)))
                    By = NP("roll", Cy, 1)
                    By[0] = 0.0
                else:
                    By = NP("array", state.count, dtype=NP.dtype(float))
                    Cy = NP(By.copy())
                Dy = NP(Ay.copy())

            else:
                if cumulative:
                    Cx = NP("cumsum",
                            NP("array", state.count, dtype=NP.dtype(float)))
                    Bx = NP("roll", Cx, 1)
                    Bx[0] = 0.0
                else:
                    Bx = NP("array", state.count, dtype=NP.dtype(float))
                    Cx = NP(Bx.copy())
                Ax = NP("zeros", len(state.count), dtype=NP.dtype(float))
                Dx = NP(Ax.copy())
                Ay = NP("array", [
                    low if low is not None else float("-inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                By = NP(Ay.copy())
                Cy = NP("array", [
                    high if high is not None else float("inf")
                    for low, high in state.edges
                ],
                        dtype=NP.dtype(float))
                Dy = NP(Cy.copy())

            AX, AY = plotCoordinates(Ax, Ay)
            BX, BY = plotCoordinates(Bx, By)
            CX, CY = plotCoordinates(Cx, Cy)
            DX, DY = plotCoordinates(Dx, Dy)

            if visualization == "skyline":
                gap = self.get("gap", defaultFromXsd=True, convertType=True)

                if vertical:
                    if gap > 0.0 and NP(
                            NP(DX - gap / 2.0) -
                            NP(AX + gap / 2.0)).min() > 0.0:
                        AX += gap / 2.0
                        BX += gap / 2.0
                        CX -= gap / 2.0
                        DX -= gap / 2.0
                else:
                    if gap > 0.0 and NP(
                            NP(AY + gap / 2.0) -
                            NP(DY - gap / 2.0)).min() > 0.0:
                        AY -= gap / 2.0
                        BY -= gap / 2.0
                        CY += gap / 2.0
                        DY += gap / 2.0

                pathdata = []
                nextIsMoveto = True
                for i in xrange(len(state.count)):
                    iprev = i - 1
                    inext = i + 1

                    if vertical and By[i] == 0.0 and Cy[i] == 0.0:
                        if i > 0 and not nextIsMoveto:
                            pathdata.append("L %r %r" % (DX[iprev], DY[iprev]))
                        nextIsMoveto = True

                    elif not vertical and Bx[i] == 0.0 and Cx[i] == 0.0:
                        if i > 0 and not nextIsMoveto:
                            pathdata.append("L %r %r" % (DX[iprev], DY[iprev]))
                        nextIsMoveto = True

                    else:
                        if nextIsMoveto or gap > 0.0 or (
                                vertical and DX[iprev] != AX[i]) or (
                                    not vertical and DY[iprev] != AY[i]):
                            pathdata.append("M %r %r" % (AX[i], AY[i]))
                            nextIsMoveto = False

                        pathdata.append("L %r %r" % (BX[i], BY[i]))
                        pathdata.append("L %r %r" % (CX[i], CY[i]))

                        if i == len(state.count) - 1 or gap > 0.0 or (
                                vertical and DX[i] != AX[inext]) or (
                                    not vertical and DY[i] != AY[inext]):
                            pathdata.append("L %r %r" % (DX[i], DY[i]))

                style = self.getStyleState()
                del style["marker-size"]
                del style["marker-outline"]
                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(style)))

            elif visualization == "polyline":
                pathdata = []
                for i in xrange(len(state.count)):
                    if i == 0:
                        pathdata.append("M %r %r" % (AX[i], AY[i]))

                    pathdata.append("L %r %r" % ((BX[i] + CX[i]) / 2.0,
                                                 (BY[i] + CY[i]) / 2.0))

                    if i == len(state.count) - 1:
                        pathdata.append("L %r %r" % (DX[i], DY[i]))

                style = self.getStyleState()
                del style["marker-size"]
                del style["marker-outline"]
                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(style)))

            elif visualization == "smooth":
                smoothingSamples = math.ceil(len(state.count) / 2.0)

                BCX = NP(NP(BX + CX) / 2.0)
                BCY = NP(NP(BY + CY) / 2.0)

                xarray = NP("array", [AX[0]] + list(BCX) + [DX[-1]],
                            dtype=NP.dtype(float))
                yarray = NP("array", [AY[0]] + list(BCY) + [DY[-1]],
                            dtype=NP.dtype(float))
                samples = NP("linspace",
                             AX[0],
                             DX[-1],
                             int(smoothingSamples),
                             endpoint=True)
                smoothingScale = abs(DX[-1] - AX[0]) / smoothingSamples

                xlist, ylist, dxlist, dylist = PlotCurve.pointsToSmoothCurve(
                    xarray, yarray, samples, smoothingScale, False)

                pathdata = PlotCurve.formatPathdata(xlist, ylist,
                                                    dxlist, dylist,
                                                    PlotCoordinates(), False,
                                                    True)

                style = self.getStyleState()
                fillStyle = dict(
                    (x, style[x]) for x in style if x.startswith("fill"))
                fillStyle["stroke"] = "none"
                strokeStyle = dict(
                    (x, style[x]) for x in style if x.startswith("stroke"))

                if style["fill"] != "none" and len(pathdata) > 0:
                    if vertical:
                        firstPoint = plotCoordinates(Ax[0], 0.0)
                        lastPoint = plotCoordinates(Dx[-1], 0.0)
                    else:
                        firstPoint = plotCoordinates(0.0, Ay[0])
                        lastPoint = plotCoordinates(0.0, Dy[-1])

                    pathdata2 = [
                        "M %r %r" % firstPoint, pathdata[0].replace("M", "L")
                    ]
                    pathdata2.extend(pathdata[1:])
                    pathdata2.append(pathdata[-1])
                    pathdata2.append("L %r %r" % lastPoint)

                    output.append(
                        svg.path(d=" ".join(pathdata2),
                                 style=PlotStyle.toString(fillStyle)))

                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(strokeStyle)))

            elif visualization == "points":
                currentStyle = PlotStyle.toDict(self.get("style") or {})
                style = self.getStyleState()
                if "fill" not in currentStyle:
                    style["fill"] = "black"

                BCX = NP(NP(BX + CX) / 2.0)
                BCY = NP(NP(BY + CY) / 2.0)

                svgId = self.get("svgId")
                if svgId is None:
                    svgIdMarker = plotDefinitions.uniqueName()
                else:
                    svgIdMarker = svgId + ".marker"

                marker = PlotScatter.makeMarker(
                    svgIdMarker, self.get("marker", defaultFromXsd=True),
                    style, self.childOfTag("PlotSvgMarker"))
                plotDefinitions[marker.get("id")] = marker

                markerReference = "#" + marker.get("id")
                output.extend(
                    svg.use(
                        **{
                            "x": repr(x),
                            "y": repr(y),
                            defs.XLINK_HREF: markerReference
                        }) for x, y in itertools.izip(BCX, BCY))

            else:
                raise NotImplementedError("TODO: add 'errorbars'")

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotHistogram draw")
        return output
Exemple #23
0
    def frame(self, dataTable, functionTable, performanceTable, plotCoordinates, plotContentBox, plotDefinitions):
        """Draw a plot frame and the plot elements it contains.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed (not the coordinate system defined by the plot).
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLayout")

        svgId = self.get("svgId")
        content = []
        if svgId is None:
            attrib = {}
        else:
            attrib = {"id": svgId}

        style = self.getStyleState()

        title = self.get("title")
        if title is not None:
            textStyle = {"stroke": "none", "fill": style["title-color"]}
            for styleProperty in (
                "font",
                "font-family",
                "font-size",
                "font-size-adjust",
                "font-stretch",
                "font-style",
                "font-variant",
                "font-weight",
            ):
                if styleProperty in style:
                    textStyle[styleProperty] = style[styleProperty]

            plotContentBox = plotContentBox.subContent(
                {
                    "margin-top": repr(
                        float(style.get("margin-top", style["margin"]))
                        + float(style["title-height"])
                        + float(style["title-gap"])
                    )
                }
            )
            content.append(
                svg.text(
                    title,
                    **{
                        "transform": "translate(%r,%r)"
                        % (plotContentBox.x + plotContentBox.width / 2.0, plotContentBox.y - float(style["title-gap"])),
                        "text-anchor": "middle",
                        defs.XML_SPACE: "preserve",
                        "style": PlotStyle.toString(textStyle),
                    }
                )
            )

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        ### background rectangle
        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {
                "x": repr(x1),
                "y": repr(y1),
                "width": repr(x2 - x1),
                "height": repr(y2 - y1),
                "style": PlotStyle.toString(rectStyle),
            }

            if rectStyle["fill"] != "none":
                if "background-opacity" in style:
                    rectStyle["fill-opacity"] = style["background-opacity"]
                if svgId is not None:
                    subAttrib["id"] = svgId + ".background"
                content.append(svg.rect(**subAttrib))

        ### sub-content
        if subContentBox is not None:
            plotFrames = self.childrenOfClass(PmmlPlotFrame)
            rows = self.get("rows", defaultFromXsd=True, convertType=True)
            cols = self.get("cols", defaultFromXsd=True, convertType=True)

            rowHeights = style["row-heights"]
            if rowHeights == "auto":
                rowHeights = [subContentBox.height / float(rows)] * rows
            else:
                try:
                    rowHeights = map(float, rowHeights.split())
                    if any(x <= 0.0 for x in rowHeights):
                        raise ValueError
                except ValueError:
                    raise defs.PmmlValidationError('If not "auto", all items in row-heights must be positive numbers')
                if len(rowHeights) != rows:
                    raise defs.PmmlValidationError(
                        "Number of elements in row-heights (%d) must be equal to rows (%d)" % (len(rowHeights), rows)
                    )

                norm = sum(rowHeights) / subContentBox.height
                rowHeights = [x / norm for x in rowHeights]

            colWidths = style["col-widths"]
            if colWidths == "auto":
                colWidths = [subContentBox.width / float(cols)] * cols
            else:
                try:
                    colWidths = map(float, colWidths.split())
                    if any(x <= 0.0 for x in colWidths):
                        raise ValueError
                except ValueError:
                    raise defs.PmmlValidationError('If not "auto", all items in col-widths must be positive numbers')
                if len(colWidths) != cols:
                    raise defs.PmmlValidationError(
                        "Number of elements in col-widths (%d) must be equal to cols (%d)" % (len(colWidths), cols)
                    )

                norm = sum(colWidths) / subContentBox.width
                colWidths = [x / norm for x in colWidths]

            plotFramesIndex = 0
            cellY = subContentBox.y
            for vertCell in xrange(rows):
                cellX = subContentBox.x
                for horizCell in xrange(cols):
                    if plotFramesIndex < len(plotFrames):
                        plotFrame = plotFrames[plotFramesIndex]

                        cellCoordinates = PlotCoordinatesOffset(plotCoordinates, cellX, cellY)
                        cellContentBox = PlotContentBox(0, 0, colWidths[horizCell], rowHeights[vertCell])

                        performanceTable.pause("PlotLayout")
                        content.append(
                            plotFrame.frame(
                                dataTable,
                                functionTable,
                                performanceTable,
                                cellCoordinates,
                                cellContentBox,
                                plotDefinitions,
                            )
                        )
                        performanceTable.unpause("PlotLayout")

                    plotFramesIndex += 1
                    cellX += colWidths[horizCell]
                cellY += rowHeights[vertCell]

        ### border rectangle (reuses subAttrib, replaces subAttrib["style"])
        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in (
                    "border-dasharray",
                    "border-dashoffset",
                    "border-linecap",
                    "border-linejoin",
                    "border-miterlimit",
                    "border-opacity",
                    "border-width",
                ):
                    if styleProperty in style:
                        rectStyle[styleProperty.replace("border-", "stroke-")] = style[styleProperty]

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        performanceTable.end("PlotLayout")
        return svg.g(*content, **attrib)
Exemple #24
0
class PlotSvgAnnotation(PmmlPlotContentAnnotation):
    """PlotSvgAnnotation represents an arbitrary SVG image as an
    annotation (a graphic that is not embedded in a PlotWindow's
    coordinate system).

    To center the PlotSvgAnnotation, set all margins to "auto".  To put
    it in a corner, set all margins to "auto" except for the desired corner
    (default is margin-right: -10; margin-top: -10; margin-left: auto;
    margin-bottom: auto).  To fill the area (hiding anything below
    it), set all margins to a specific value.

    PMML subelements:

      - SvgBinding for inline SVG.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - fileName: for external SVG.
      - style: CSS style properties.

    Inline and external SVG are mutually exclusive.

    CSS properties:

      - margin-top, margin-right, margin-bottom, margin-left,
        margin: space between the enclosure and the border.
      - border-top-width, border-right-width, border-bottom-width,
        border-left-width, border-width: thickness of the border.
      - padding-top, padding-right, padding-bottom, padding-left,
        padding: space between the border and the inner content.
      - background, background-opacity: color of the background.
      - border-color, border-dasharray, border-dashoffset,
        border-linecap, border-linejoin, border-miterlimit,
        border-opacity, border-width: properties of the border line.

    See the source code for the full XSD.
    """

    styleProperties = [
        "margin-top",
        "margin-right",
        "margin-bottom",
        "margin-left",
        "border-top-width",
        "border-right-width",
        "border-bottom-width",
        "border-left-width",
        "border-width",
        "padding-top",
        "padding-right",
        "padding-bottom",
        "padding-left",
        "padding",
        "border-color",
        "border-dasharray",
        "border-dashoffset",
        "border-linecap",
        "border-linejoin",
        "border-miterlimit",
        "border-opacity",
        "border-width",
    ]

    styleDefaults = {
        "border-color": "none",
        "margin-right": "10",
        "margin-top": "10",
        "padding": "0"
    }

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotSvgAnnotation">
        <xs:complexType>
            <xs:complexContent>
                <xs:restriction base="xs:anyType">
                    <xs:sequence>
                        <xs:any minOccurs="0" maxOccurs="1" processContents="skip" />
                    </xs:sequence>
                    <xs:attribute name="svgId" type="xs:string" use="optional" />
                    <xs:attribute name="fileName" type="xs:string" use="optional" />
                    <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
                </xs:restriction>
            </xs:complexContent>
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    @staticmethod
    def findSize(svgBinding):
        """Determine the bounding box of an SVG image.

        @type svgBinding: SvgBinding
        @param svgBinding: The SVG image.
        @rtype: 4-tuple of numbers
        @return: C{xmin}, C{ymin}, C{xmax}, C{ymax}
        """

        viewBox = svgBinding.get("viewBox")

        if viewBox is not None:
            return map(float, viewBox.split())

        else:
            xmax, ymax = None, None
            for item in svgBinding.iterdescendants():
                try:
                    x = float(item.get("x"))
                except (ValueError, TypeError):
                    pass
                else:
                    if xmax is None or x > xmax: xmax = x

                try:
                    x = float(item.get("x")) + float(item.get("width"))
                except (ValueError, TypeError):
                    pass
                else:
                    if xmax is None or x > xmax: xmax = x

                try:
                    x = float(item.get("x1"))
                except (ValueError, TypeError):
                    pass
                else:
                    if xmax is None or x > xmax: xmax = x

                try:
                    x = float(item.get("x2"))
                except (ValueError, TypeError):
                    pass
                else:
                    if xmax is None or x > xmax: xmax = x

                try:
                    y = float(item.get("y"))
                except (ValueError, TypeError):
                    pass
                else:
                    if ymax is None or y > ymax: ymax = y

                try:
                    y = float(item.get("y")) + float(item.get("height"))
                except (ValueError, TypeError):
                    pass
                else:
                    if ymax is None or y > ymax: ymax = y

                try:
                    y = float(item.get("y1"))
                except (ValueError, TypeError):
                    pass
                else:
                    if ymax is None or y > ymax: ymax = y

                try:
                    y = float(item.get("y2"))
                except (ValueError, TypeError):
                    pass
                else:
                    if ymax is None or y > ymax: ymax = y

                d = item.get("d")
                if d is not None:
                    for m in re.finditer(
                            "[A-Za-z]\s*([0-9\.\-+eE]+)[\s+,]([0-9\.\-+eE]+)",
                            d):
                        x, y = float(m.group(1)), float(m.group(2))

                        if xmax is None or x > xmax: xmax = x
                        if ymax is None or y > ymax: ymax = y

            if xmax is None: xmax = 1
            if ymax is None: ymax = 1

            return 0, 0, xmax, ymax

    def draw(self, dataTable, functionTable, performanceTable, plotCoordinates,
             plotContentBox, plotDefinitions):
        """Draw the plot annotation.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed.
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker

        svgId = self.get("svgId")
        if svgId is None:
            output = svg.g()
        else:
            output = svg.g(**{"id": svgId})
        content = [output]

        inlineSvg = self.getchildren()
        fileName = self.get("fileName")
        if len(inlineSvg) == 1 and fileName is None:
            svgBinding = inlineSvg[0]
        elif len(inlineSvg) == 0 and fileName is not None:
            svgBinding = SvgBinding.loadXml(fileName)
        else:
            raise defs.PmmlValidationError(
                "PlotSvgAnnotation should specify an inline SVG or a fileName but not both or neither"
            )

        style = self.getStyleState()

        if style.get("margin-bottom") == "auto": del style["margin-bottom"]
        if style.get("margin-top") == "auto": del style["margin-top"]
        if style.get("margin-left") == "auto": del style["margin-left"]
        if style.get("margin-right") == "auto": del style["margin-right"]

        subContentBox = plotContentBox.subContent(style)
        sx1, sy1, sx2, sy2 = PlotSvgAnnotation.findSize(svgBinding)
        nominalHeight = sy2 - sy1
        nominalWidth = sx2 - sx1

        if nominalHeight < subContentBox.height:
            if "margin-bottom" in style and "margin-top" in style:
                pass
            elif "margin-bottom" in style:
                style["margin-top"] = subContentBox.height - nominalHeight
            elif "margin-top" in style:
                style["margin-bottom"] = subContentBox.height - nominalHeight
            else:
                style["margin-bottom"] = style["margin-top"] = (
                    subContentBox.height - nominalHeight) / 2.0

        if nominalWidth < subContentBox.width:
            if "margin-left" in style and "margin-right" in style:
                pass
            elif "margin-left" in style:
                style["margin-right"] = subContentBox.width - nominalWidth
            elif "margin-right" in style:
                style["margin-left"] = subContentBox.width - nominalWidth
            else:
                style["margin-left"] = style["margin-right"] = (
                    subContentBox.width - nominalWidth) / 2.0

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        if subContentBox is not None:
            tx1, ty1 = plotCoordinates(subContentBox.x, subContentBox.y)
            tx2, ty2 = plotCoordinates(subContentBox.x + subContentBox.width,
                                       subContentBox.y + subContentBox.height)

            output.extend([copy.deepcopy(x) for x in svgBinding.getchildren()])

            output["transform"] = "translate(%r, %r) scale(%r, %r)" % (
                tx1 - sx1, ty1 - sy1, (tx2 - tx1) / float(sx2 - sx1),
                (ty2 - ty1) / float(sy2 - sy1))

        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace(
                            "border-", "stroke-")] = style[styleProperty]

                x1 = borderRect.x
                y1 = borderRect.y
                x2 = borderRect.x + borderRect.width
                y2 = borderRect.y + borderRect.height
                x1, y1 = plotCoordinates(x1, y1)
                x2, y2 = plotCoordinates(x2, y2)

                subAttrib = {
                    "x": repr(x1),
                    "y": repr(y1),
                    "width": repr(x2 - x1),
                    "height": repr(y2 - y1),
                    "style": PlotStyle.toString(rectStyle)
                }

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        return svg.g(*content)
Exemple #25
0
class PlotLayout(PmmlPlotFrame):
    """PlotLayout arranges plots (or nested PlotLayouts) on a page.
    
    It has CSS properties to stylize the space between plots,
    including a margin, border, and padding following the CSS box
    model.

    PMML subelements:

      - Any PLOT-FRAMEs (PmmlPlotFrames)

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - rows: number of rows in the layout grid.
      - cols: number of columns in the layout grid.
      - title: global title above the layout grid.
      - style: CSS style properties.

    CSS properties:

      - margin-top, margin-right, margin-bottom, margin-left,
        margin: space between the enclosure and the border.
      - border-top-width, border-right-width, border-bottom-width,
        border-left-width, border-width: thickness of the border.
      - padding-top, padding-right, padding-bottom, padding-left,
        padding: space between the border and the inner content.
      - row-heights, col-widths: space-delimited array of relative
        heights and widths of each row and column, respectively;
        use C{"auto"} for equal divisions (the default); raises an
        error if the number of elements in the array is not equal
        to C{rows} or C{cols}, respectively.
      - title-height, title-gap: height of and gap below the global
        title.
      - background, background-opacity: color of the background.
      - border-color, border-dasharray, border-dashoffset,
        border-linecap, border-linejoin, border-miterlimit,
        border-opacity, border-width: properties of the border line.
      - font, font-family, font-size, font-size-adjust, font-stretch,
        font-style, font-variant, font-weight: properties of the
        title font.

    See the source code for the full XSD.
    """

    styleProperties = [
        "margin-top",
        "margin-right",
        "margin-bottom",
        "margin-left",
        "margin",
        "border-top-width",
        "border-right-width",
        "border-bottom-width",
        "border-left-width",
        "border-width",
        "padding-top",
        "padding-right",
        "padding-bottom",
        "padding-left",
        "padding",
        "row-heights",
        "col-widths",
        "title-height",
        "title-gap",
        "background",
        "background-opacity",
        "border-color",
        "border-dasharray",
        "border-dashoffset",
        "border-linecap",
        "border-linejoin",
        "border-miterlimit",
        "border-opacity",
        "border-width",
        "font",
        "font-family",
        "font-size",
        "font-size-adjust",
        "font-stretch",
        "font-style",
        "font-variant",
        "font-weight",
    ]

    styleDefaults = {
        "background": "none",
        "border-color": "none",
        "margin": "2",
        "padding": "2",
        "border-width": "0",
        "row-heights": "auto",
        "col-widths": "auto",
        "title-height": "30",
        "title-gap": "5",
        "title-color": "black",
        "font-size": "30.0"
    }

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotLayout">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:group ref="PLOT-FRAME" minOccurs="0" maxOccurs="unbounded" />
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="rows" type="xs:positiveInteger" use="required" />
            <xs:attribute name="cols" type="xs:positiveInteger" use="required" />
            <xs:attribute name="title" type="xs:string" use="optional" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    def frame(self, dataTable, functionTable, performanceTable,
              plotCoordinates, plotContentBox, plotDefinitions):
        """Draw a plot frame and the plot elements it contains.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed (not the coordinate system defined by the plot).
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLayout")

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        style = self.getStyleState()

        title = self.get("title")
        if title is not None:
            textStyle = {"stroke": "none", "fill": style["title-color"]}
            for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
                if styleProperty in style:
                    textStyle[styleProperty] = style[styleProperty]

            plotContentBox = plotContentBox.subContent({
                "margin-top":
                repr(
                    float(style.get("margin-top", style["margin"])) +
                    float(style["title-height"]) + float(style["title-gap"]))
            })
            content.append(
                svg.text(
                    title, **{
                        "transform":
                        "translate(%r,%r)" %
                        (plotContentBox.x + plotContentBox.width / 2.0,
                         plotContentBox.y - float(style["title-gap"])),
                        "text-anchor":
                        "middle",
                        defs.XML_SPACE:
                        "preserve",
                        "style":
                        PlotStyle.toString(textStyle)
                    }))

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        ### background rectangle
        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {
                "x": repr(x1),
                "y": repr(y1),
                "width": repr(x2 - x1),
                "height": repr(y2 - y1),
                "style": PlotStyle.toString(rectStyle)
            }

            if rectStyle["fill"] != "none":
                if "background-opacity" in style:
                    rectStyle["fill-opacity"] = style["background-opacity"]
                if svgId is not None:
                    subAttrib["id"] = svgId + ".background"
                content.append(svg.rect(**subAttrib))

        ### sub-content
        if subContentBox is not None:
            plotFrames = self.childrenOfClass(PmmlPlotFrame)
            rows = self.get("rows", defaultFromXsd=True, convertType=True)
            cols = self.get("cols", defaultFromXsd=True, convertType=True)

            rowHeights = style["row-heights"]
            if rowHeights == "auto":
                rowHeights = [subContentBox.height / float(rows)] * rows
            else:
                try:
                    rowHeights = map(float, rowHeights.split())
                    if any(x <= 0.0 for x in rowHeights): raise ValueError
                except ValueError:
                    raise defs.PmmlValidationError(
                        "If not \"auto\", all items in row-heights must be positive numbers"
                    )
                if len(rowHeights) != rows:
                    raise defs.PmmlValidationError(
                        "Number of elements in row-heights (%d) must be equal to rows (%d)"
                        % (len(rowHeights), rows))

                norm = sum(rowHeights) / subContentBox.height
                rowHeights = [x / norm for x in rowHeights]

            colWidths = style["col-widths"]
            if colWidths == "auto":
                colWidths = [subContentBox.width / float(cols)] * cols
            else:
                try:
                    colWidths = map(float, colWidths.split())
                    if any(x <= 0.0 for x in colWidths): raise ValueError
                except ValueError:
                    raise defs.PmmlValidationError(
                        "If not \"auto\", all items in col-widths must be positive numbers"
                    )
                if len(colWidths) != cols:
                    raise defs.PmmlValidationError(
                        "Number of elements in col-widths (%d) must be equal to cols (%d)"
                        % (len(colWidths), cols))

                norm = sum(colWidths) / subContentBox.width
                colWidths = [x / norm for x in colWidths]

            plotFramesIndex = 0
            cellY = subContentBox.y
            for vertCell in xrange(rows):
                cellX = subContentBox.x
                for horizCell in xrange(cols):
                    if plotFramesIndex < len(plotFrames):
                        plotFrame = plotFrames[plotFramesIndex]

                        cellCoordinates = PlotCoordinatesOffset(
                            plotCoordinates, cellX, cellY)
                        cellContentBox = PlotContentBox(
                            0, 0, colWidths[horizCell], rowHeights[vertCell])

                        performanceTable.pause("PlotLayout")
                        content.append(
                            plotFrame.frame(dataTable, functionTable,
                                            performanceTable, cellCoordinates,
                                            cellContentBox, plotDefinitions))
                        performanceTable.unpause("PlotLayout")

                    plotFramesIndex += 1
                    cellX += colWidths[horizCell]
                cellY += rowHeights[vertCell]

        ### border rectangle (reuses subAttrib, replaces subAttrib["style"])
        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace(
                            "border-", "stroke-")] = style[styleProperty]

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        performanceTable.end("PlotLayout")
        return svg.g(*content, **attrib)
Exemple #26
0
    def frame(self, dataTable, functionTable, performanceTable, plotCoordinates, plotContentBox, plotDefinitions):
        """Draw a plot frame and the plot elements it contains.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed (not the coordinate system defined by the plot).
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotWindow")

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        style = self.getStyleState()
        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        adjustForColorScale = []

        ### draw the background

        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            if "background-opacity" in style:
                rectStyle["fill-opacity"] = style["background-opacity"]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {"x": repr(x1), "y": repr(y1), "width": repr(x2 - x1), "height": repr(y2 - y1), "style": PlotStyle.toString(rectStyle)}
            if svgId is not None:
                subAttrib["id"] = svgId + ".background"
            if rectStyle["fill"] != "none":
                r = svg.rect(**subAttrib)
                content.append(r)
                adjustForColorScale.append(r)

        sawAnnotation = False
        aboveTicks = []
        if subContentBox is not None:
            ### create a clipping region for the contents

            if svgId is None:
                svgIdClip = plotDefinitions.uniqueName()
            else:
                svgIdClip = svgId + ".clip"

            r = svg.rect(x=repr(x1), y=repr(y1), width=repr(x2 - x1), height=repr(y2 - y1))
            clipPath = svg.clipPath(r, id=svgIdClip)
            plotDefinitions[svgIdClip] = clipPath
            adjustForColorScale.append(r)

            x1 = subContentBox.x
            y1 = subContentBox.y
            x2 = subContentBox.x + subContentBox.width
            y2 = subContentBox.y + subContentBox.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            clippedDataAttrib = {"clip-path": "url(#%s)" % svgIdClip}

            ### handle the contents

            xticksSourceIndex = self.get("xticks-source", defaultFromXsd=True, convertType=True)
            yticksSourceIndex = self.get("yticks-source", defaultFromXsd=True, convertType=True)
            topticksSourceIndex = self.get("topticks-source", defaultFromXsd=True, convertType=True)
            rightticksSourceIndex = self.get("rightticks-source", defaultFromXsd=True, convertType=True)
            colorticksSourceIndex = self.get("colorticks-source", defaultFromXsd=True, convertType=True)
            xticksSource = None
            yticksSource = None
            topticksSource = None
            rightticksSource = None
            colorticksSource = None

            states = {}
            for coordinatesIndex, overlay in enumerate(self.childrenOfClass(PlotOverlay)):
                plotContents = overlay.childrenOfClass(PmmlPlotContent)

                xlog = overlay.get("xlog", defaultFromXsd=True, convertType=True)
                ylog = overlay.get("ylog", defaultFromXsd=True, convertType=True)
                zlog = overlay.get("zlog", defaultFromXsd=True, convertType=True)
                plotRange = PlotRange(xStrictlyPositive=xlog, yStrictlyPositive=ylog, zStrictlyPositive=zlog)

                ### calculate the contents' coordinates to determine ranges
                performanceTable.pause("PlotWindow")
                for plotContent in plotContents:
                    states[plotContent] = self._State()
                    plotContent.prepare(states[plotContent], dataTable, functionTable, performanceTable, plotRange)
                performanceTable.unpause("PlotWindow")

                xmin, ymin, xmax, ymax = plotRange.ranges()
                xmin = float(overlay.get("xmin", xmin))
                ymin = float(overlay.get("ymin", ymin))
                xmax = float(overlay.get("xmax", xmax))
                ymax = float(overlay.get("ymax", ymax))

                zmin, zmax = plotRange.zranges()

                if zmin is None or zmax is None:
                    zmin = None
                    zmax = None
                else:
                    zmin = float(overlay.get("zmin", zmin))
                    zmax = float(overlay.get("zmax", zmax))

                ### create the inner coordinate system
                plotCoordinatesWindow = PlotCoordinatesWindow(plotCoordinates, xmin, ymin, xmax, ymax, subContentBox.x, subContentBox.y, subContentBox.width, subContentBox.height, flipy=True, xlog=xlog, ylog=ylog, xfieldType=plotRange.xfieldType, yfieldType=plotRange.yfieldType, xstrings=plotRange.xstrings, ystrings=plotRange.ystrings)

                if coordinatesIndex + 1 == xticksSourceIndex:
                    xticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == yticksSourceIndex:
                    yticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == topticksSourceIndex:
                    topticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == rightticksSourceIndex:
                    rightticksSource = plotCoordinatesWindow
                if coordinatesIndex + 1 == colorticksSourceIndex:
                    colorticksSource = (zmin, zmax, zlog, plotRange.zfieldType)

                for plotContent in plotContents:
                    states[plotContent].plotCoordinatesWindow = plotCoordinatesWindow

            ### figure out if you have any color ticks, since the color tick box shifts the contents
            if colorticksSource is None or zmin is None or zmax is None:
                colorticksSource = None
                colorticksDraw = "nothing"
                colorticks, colorminiticks = None, None
            else:
                zmin, zmax, zlog, cfieldType = colorticksSource

                if zmin is None or zmax is None:
                    colorticksDraw = "nothing"
                    colorticks, colorminiticks = None, None
                else:
                    colorticksDraw = self.get("colorticks-draw", defaultFromXsd=True)
                    tickSpecification = self.get("colorticks", defaultFromXsd=True)

                    if tickSpecification == "auto":
                        if cfieldType.istemporal():
                            colorticks, colorminiticks = PlotTickMarks.interpret("time()", zmin, zmax)
                        elif zlog:
                            colorticks, colorminiticks = PlotTickMarks.interpret("log(~10)", zmin, zmax)
                        else:
                            colorticks, colorminiticks = PlotTickMarks.interpret("linear(~10)", zmin, zmax)
                    elif tickSpecification == "none":
                        colorticks, colorminiticks = {}, []
                    else:
                        colorticks, colorminiticks = PlotTickMarks.interpret(tickSpecification, zmin, zmax)

            gradient = self.childrenOfTag("PlotGradientStop")
            lastStop = None
            for plotGradientStop in gradient:
                offset = float(plotGradientStop["offset"])
                if lastStop is not None and offset <= lastStop:
                    raise defs.PmmlValidationError("Sequence of PlotGradientStop must be strictly increasing in \"offset\"")
                lastStop = offset

            xshiftForColorScale = 0.0
            if colorticksDraw != "nothing":
                xshiftForColorScale += float(style["colorscale-width"]) + float(style["colortick-label-xoffset"]) + float(style["margin-colorright"])
                if self["colorlabel"] is not None:
                    xshiftForColorScale += float(style.get("colorlabel-margin", style["label-margin"]))

            if colorticksSource is not None:
                colorticksSource = PlotCoordinatesWindow(plotCoordinates, 0.0, zmin, 1.0, zmax, subContentBox.x + subContentBox.width - xshiftForColorScale, subContentBox.y, xshiftForColorScale, subContentBox.height, flipy=True, xlog=False, ylog=zlog, xfieldType=cfieldType, yfieldType=cfieldType, xstrings=[], ystrings=[])

                cx2 = plotCoordinates(borderRect.x + borderRect.width, borderRect.y)[0] - float(style["margin-colorright"])
                if self["colorlabel"] is not None:
                    cx2 -= float(style.get("colorlabel-margin", style["label-margin"]))

            for r in adjustForColorScale:
                r["width"] = repr(float(r["width"]) - xshiftForColorScale)

            subContentBox.width -= xshiftForColorScale
            borderRect.width -= xshiftForColorScale

            done = set()
            if xticksSource is not None:
                xticksSource.outerX2 -= xshiftForColorScale
                done.add(xticksSource)
            if yticksSource is not None and yticksSource not in done:
                yticksSource.outerX2 -= xshiftForColorScale
                done.add(yticksSource)
            if topticksSource is not None and topticksSource not in done:
                topticksSource.outerX2 -= xshiftForColorScale
                done.add(topticksSource)
            if rightticksSource is not None and rightticksSource not in done:
                rightticksSource.outerX2 -= xshiftForColorScale
                done.add(rightticksSource)

            ### actually draw the contents and the non-coordinate annotations
            annotationCoordinates = PlotCoordinatesOffset(plotCoordinates, subContentBox.x, subContentBox.y)
            annotationBox = PlotContentBox(0, 0, subContentBox.width, subContentBox.height)

            for overlayOrAnnotation in self.getchildren():
                performanceTable.pause("PlotWindow")

                whatToDraw = []
                if isinstance(overlayOrAnnotation, PlotOverlay):
                    plotContents = overlayOrAnnotation.childrenOfClass(PmmlPlotContent)
                    for plotContent in plotContents:
                        plotCoordinatesWindow = states[plotContent].plotCoordinatesWindow
                        if zmin is not None and zmax is not None:
                            plotCoordinatesWindow.zmin = zmin
                            plotCoordinatesWindow.zmax = zmax
                            plotCoordinatesWindow.zlog = zlog
                            plotCoordinatesWindow.gradient = gradient

                        whatToDraw.append(svg.g(plotContent.draw(states[plotContent], plotCoordinatesWindow, plotDefinitions, performanceTable), **clippedDataAttrib))

                elif isinstance(overlayOrAnnotation, PmmlPlotContentAnnotation):
                    whatToDraw.append(overlayOrAnnotation.draw(dataTable, functionTable, performanceTable, annotationCoordinates, annotationBox, plotDefinitions))
                    sawAnnotation = True

                if sawAnnotation:
                    aboveTicks.extend(whatToDraw)
                else:
                    content.extend(whatToDraw)

                performanceTable.unpause("PlotWindow")

            del states

        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}

            for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                if styleProperty in style:
                    rectStyle[styleProperty.replace("border-", "stroke-")] = style[styleProperty]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {"x": repr(x1), "y": repr(y1), "width": repr(x2 - x1), "height": repr(y2 - y1), "style": PlotStyle.toString(rectStyle)}
            if svgId is not None:
                subAttrib["id"] = svgId + ".border"

            ### draw the tick-marks and axis labels

            leftEdge, topEdge = plotCoordinates(plotContentBox.x, plotContentBox.y)
            rightEdge, bottomEdge = plotCoordinates(plotContentBox.x + plotContentBox.width, plotContentBox.y + plotContentBox.height)

            performanceTable.begin("tickmarks")

            if xticksSource is None:
                xticks = {}
                xminiticks = []
            else:
                tickSpecification = self.get("xticks", defaultFromXsd=True)
                if tickSpecification == "auto":
                    if xticksSource.xfieldType.isstring():
                        xticks, xminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(xticksSource.xstrings)), xticksSource.innerX1, xticksSource.innerX2)
                    elif xticksSource.xfieldType.istemporal():
                        xticks, xminiticks = PlotTickMarks.interpret("time()", xticksSource.innerX1, xticksSource.innerX2)
                    elif xticksSource.xlog:
                        xticks, xminiticks = PlotTickMarks.interpret("log(~10)", xticksSource.innerX1, xticksSource.innerX2)
                    else:
                        xticks, xminiticks = PlotTickMarks.interpret("linear(~10)", xticksSource.innerX1, xticksSource.innerX2)
                elif tickSpecification == "none":
                    xticks, xminiticks = {}, []
                else:
                    xticks, xminiticks = PlotTickMarks.interpret(tickSpecification, xticksSource.innerX1, xticksSource.innerX2)

            if yticksSource is None:
                yticks = {}
                yminiticks = []
            else:
                tickSpecification = self.get("yticks", defaultFromXsd=True)
                if tickSpecification == "auto":
                    if yticksSource.yfieldType.isstring():
                        yticks, yminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(yticksSource.ystrings)), yticksSource.innerY1, yticksSource.innerY2)
                    elif yticksSource.yfieldType.istemporal():
                        yticks, yminiticks = PlotTickMarks.interpret("time()", yticksSource.innerY1, yticksSource.innerY2)
                    elif yticksSource.ylog:
                        yticks, yminiticks = PlotTickMarks.interpret("log(~10)", yticksSource.innerY1, yticksSource.innerY2)
                    else:
                        yticks, yminiticks = PlotTickMarks.interpret("linear(~10)", yticksSource.innerY1, yticksSource.innerY2)
                elif tickSpecification == "none":
                    yticks, yminiticks = {}, []
                else:
                    yticks, yminiticks = PlotTickMarks.interpret(tickSpecification, yticksSource.innerY1, yticksSource.innerY2)

            if topticksSource is None:
                topticks = {}
                topminiticks = []
            else:
                tickSpecification = self.get("topticks", defaultFromXsd=True)
                if tickSpecification == "auto" and topticksSource == xticksSource:
                    topticks, topminiticks = xticks, xminiticks

                elif tickSpecification == "auto":
                    if topticksSource.xfieldType.isstring():
                        topticks, topminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(topticksSource.xstrings)), topticksSource.innerX1, topticksSource.innerX2)
                    elif topticksSource.xfieldType.istemporal():
                        topticks, topminiticks = PlotTickMarks.interpret("time()", topticksSource.innerX1, topticksSource.innerX2)
                    elif topticksSource.xlog:
                        topticks, topminiticks = PlotTickMarks.interpret("log(~10)", topticksSource.innerX1, topticksSource.innerX2)
                    else:
                        topticks, topminiticks = PlotTickMarks.interpret("linear(~10)", topticksSource.innerX1, topticksSource.innerX2)
                    

                elif tickSpecification == "none":
                    topticks, topminiticks = {}, []

                else:
                    topticks, topminiticks = PlotTickMarks.interpret(tickSpecification, topticksSource.innerX1, topticksSource.innerX2)

            if rightticksSource is None:
                rightticks = {}
                rightminiticks = []
            else:
                tickSpecification = self.get("rightticks", defaultFromXsd=True)
                if tickSpecification == "auto" and rightticksSource == yticksSource:
                    rightticks, rightminiticks = yticks, yminiticks

                elif tickSpecification == "auto":
                    if rightticksSource.yfieldType.isstring():
                        rightticks, rightminiticks = PlotTickMarks.interpret("explicit({%s})" % ", ".join("%d: \"%s\"" % (i, x) for i, x in enumerate(rightticksSource.ystrings)), rightticksSource.innerY1, rightticksSource.innerY2)
                    elif rightticksSource.yfieldType.istemporal():
                        rightticks, rightminiticks = PlotTickMarks.interpret("time()", rightticksSource.innerY1, rightticksSource.innerY2)
                    elif rightticksSource.ylog:
                        rightticks, rightminiticks = PlotTickMarks.interpret("log(~10)", rightticksSource.innerY1, rightticksSource.innerY2)
                    else:
                        rightticks, rightminiticks = PlotTickMarks.interpret("linear(~10)", rightticksSource.innerY1, rightticksSource.innerY2)

                elif tickSpecification == "none":
                    rightticks, rightminiticks = {}, []

                else:
                    rightticks, rightminiticks = PlotTickMarks.interpret(tickSpecification, rightticksSource.innerY1, rightticksSource.innerY2)

            textStyle = {"stroke": "none"}
            for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
                if styleProperty in style:
                    textStyle[styleProperty] = style[styleProperty]

            # very few SVG renderers do dominant-baseline: middle, alignment-baseline: middle, baseline-shift: middle, etc., so we have to emulate it
            dyMiddle = repr(0.35*float(style["font-size"]))

            # x (bottom) ticks
            xticksDraw = self.get("xticks-draw", defaultFromXsd=True)
            if xticksSource is not None and xticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    xticksGroup = svg.g()
                else:
                    xticksGroup = svg.g(id=(svgId + ".xticks"))

                xticklabelColor = style.get("xticklabel-color", style["ticklabel-color"])
                xtickColor = style.get("xtick-color", style["tick-color"])
                xtickLength = float(style.get("xtick-length", style["tick-length"]))
                xminitickLength = float(style.get("xminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (rightEdge - leftEdge)
                transformedTicks = dict((xticksSource(x, 1.0)[0], label) for x, label in xticks.items())
                transformedMiniticks = [xticksSource(x, 1.0)[0] for x in xminiticks if x not in xticks]
                xticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y2, x, y2 - xminitickLength) for x in transformedMiniticks if x1 + eps < x < x2 - eps), style=("stroke: %s; fill: none" % xtickColor)))
                xticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y2, x, y2 - xtickLength) for x in transformedTicks if x1 + eps < x < x2 - eps), style="stroke: %s; fill: none" % xtickColor))

                if xticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["xtick-label-xoffset"]), float(style["xtick-label-yoffset"])
                    textStyle["fill"] = xticklabelColor

                    for x, label in transformedTicks.items():
                        if x1 - eps < x < x2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if xticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x + xoffset, y2 + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif xticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(-90)" % (x + xoffset, y2 + yoffset)
                                labelAttributes["text-anchor"] = "end"
                            xticksGroup.append(svg.text(label, **labelAttributes))

                content.append(xticksGroup)

            # x (bottom) label
            xlabel = self.get("xlabel", "")
            if xlabel != "":
                labelMargin = float(style.get("xlabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("xlabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r)" % ((x1 + x2)/2.0, bottomEdge - labelMargin), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(xlabel, **labelAttributes))

            # y (left) ticks
            yticksDraw = self.get("yticks-draw", defaultFromXsd=True)
            if yticksSource is not None and yticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    yticksGroup = svg.g()
                else:
                    yticksGroup = svg.g(id=(svgId + ".yticks"))

                yticklabelColor = style.get("yticklabel-color", style["ticklabel-color"])
                ytickColor = style.get("ytick-color", style["tick-color"])
                ytickLength = float(style.get("ytick-length", style["tick-length"]))
                yminitickLength = float(style.get("yminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (bottomEdge - topEdge)
                transformedTicks = dict((yticksSource(1.0, y)[1], label) for y, label in yticks.items())
                transformedMiniticks = [yticksSource(1.0, y)[1] for y in yminiticks if y not in yticks]
                yticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x1, y, x1 + yminitickLength, y) for y in transformedMiniticks if y1 + eps < y < y2 - eps), style=("stroke: %s; fill: none" % ytickColor)))
                yticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x1, y, x1 + ytickLength, y) for y in transformedTicks if y1 + eps < y < y2 - eps), style="stroke: %s; fill: none" % ytickColor))

                if yticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["ytick-label-xoffset"]), float(style["ytick-label-yoffset"])
                    textStyle["fill"] = yticklabelColor

                    for y, label in transformedTicks.items():
                        if y1 - eps < y < y2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if yticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(-90)" % (x1 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif yticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x1 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "end"
                            yticksGroup.append(svg.text(label, **labelAttributes))

                content.append(yticksGroup)

            # y (left) label
            ylabel = self.get("ylabel", "")
            if ylabel != "":
                labelMargin = float(style.get("ylabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("ylabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r) rotate(-90)" % (leftEdge + labelMargin, (y1 + y2)/2.0), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(ylabel, **labelAttributes))

            # top ticks
            topticksDraw = self.get("topticks-draw", defaultFromXsd=True)
            if topticksSource is not None and topticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    topticksGroup = svg.g()
                else:
                    topticksGroup = svg.g(id=(svgId + ".topticks"))

                topticklabelColor = style.get("topticklabel-color", style["ticklabel-color"])
                toptickColor = style.get("toptick-color", style["tick-color"])
                toptickLength = float(style.get("toptick-length", style["tick-length"]))
                topminitickLength = float(style.get("topminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (rightEdge - leftEdge)
                transformedTicks = dict((topticksSource(x, 1.0)[0], label) for x, label in topticks.items())
                transformedMiniticks = [topticksSource(x, 1.0)[0] for x in topminiticks if x not in topticks]
                topticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y1, x, y1 + topminitickLength) for x in transformedMiniticks if x1 + eps < x < x2 - eps), style=("stroke: %s; fill: none" % toptickColor)))
                topticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x, y1, x, y1 + toptickLength) for x in transformedTicks if x1 + eps < x < x2 - eps), style="stroke: %s; fill: none" % toptickColor))

                if topticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["toptick-label-xoffset"]), float(style["toptick-label-yoffset"])
                    textStyle["fill"] = topticklabelColor

                    for x, label in transformedTicks.items():
                        if x1 - eps < x < x2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if topticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x + xoffset, y1 + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif topticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(-90)" % (x + xoffset, y1 + yoffset)
                                labelAttributes["text-anchor"] = "start"
                            topticksGroup.append(svg.text(label, **labelAttributes))

                content.append(topticksGroup)

            # top label
            toplabel = self.get("toplabel", "")
            if toplabel != "":
                labelMargin = float(style.get("toplabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("toplabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r)" % ((x1 + x2)/2.0, topEdge + labelMargin), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(toplabel, **labelAttributes))

            # right ticks
            rightticksDraw = self.get("rightticks-draw", defaultFromXsd=True)
            if rightticksSource is not None and rightticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    rightticksGroup = svg.g()
                else:
                    rightticksGroup = svg.g(id=(svgId + ".rightticks"))

                rightticklabelColor = style.get("rightticklabel-color", style["ticklabel-color"])
                righttickColor = style.get("righttick-color", style["tick-color"])
                righttickLength = float(style.get("righttick-length", style["tick-length"]))
                rightminitickLength = float(style.get("rightminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (bottomEdge - topEdge)
                transformedTicks = dict((rightticksSource(1.0, y)[1], label) for y, label in rightticks.items())
                transformedMiniticks = [rightticksSource(1.0, y)[1] for y in rightminiticks if y not in rightticks]
                rightticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x2, y, x2 - rightminitickLength, y) for y in transformedMiniticks if y1 + eps < y < y2 - eps), style=("stroke: %s; fill: none" % righttickColor)))
                rightticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (x2, y, x2 - righttickLength, y) for y in transformedTicks if y1 + eps < y < y2 - eps), style="stroke: %s; fill: none" % righttickColor))

                if rightticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["righttick-label-xoffset"]), float(style["righttick-label-yoffset"])
                    textStyle["fill"] = rightticklabelColor

                    for y, label in transformedTicks.items():
                        if y1 - eps < y < y2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if rightticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(90)" % (x2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif rightticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (x2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "start"
                            rightticksGroup.append(svg.text(label, **labelAttributes))

                content.append(rightticksGroup)

            # right label
            rightlabel = self.get("rightlabel", "")
            if rightlabel != "":
                labelMargin = float(style.get("rightlabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("rightlabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r) rotate(90)" % (rightEdge - labelMargin, (y1 + y2)/2.0), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(rightlabel, **labelAttributes))

            # color ticks
            if colorticksSource is not None and colorticksDraw in ("ticks-only", "parallel-labels", "perpendicular-labels"):
                if svgId is None:
                    colorticksGroup = svg.g()
                else:
                    colorticksGroup = svg.g(id=(svgId + ".colorticks"))

                if len(gradient) == 0:
                    linearGradient = svg.linearGradient(id=plotDefinitions.uniqueName(), x1="0%", y1="100%", x2="0%", y2="0%")
                    linearGradient.append(svg.stop(offset="0%", style="stop-color:rgb(255,255,255); stop-opacity: 1.0;"))
                    linearGradient.append(svg.stop(offset="100%", style="stop-color:rgb(0,0,255); stop-opacity: 1.0;"))
                else:
                    linearGradient = svg.linearGradient(id=plotDefinitions.uniqueName(), x1="0%", y1="100%", x2="0%", y2="0%")
                    for stop in gradient:
                        offset = "%r%%" % (100.0 * float(stop["offset"]))
                        gradientStyle = "stop-color:rgb(%r,%r,%r);" % (min(int(math.floor(256.0 * float(stop["red"]))), 255), min(int(math.floor(256.0 * float(stop["green"]))), 255), min(int(math.floor(256.0 * float(stop["blue"]))), 255))
                        opacity = stop.get("opacity")
                        if opacity is not None:
                            gradientStyle += " stop-opacity: %s;" % opacity
                        linearGradient.append(svg.stop(offset=offset, style=gradientStyle))

                plotDefinitions[linearGradient["id"]] = linearGradient

                gradientStyle = rectStyle.copy()
                gradientStyle["fill"] = "url(#%s)" % linearGradient["id"]
                colorticksGroup.append(svg.rect(**{"x": repr(cx2 - float(style["colorscale-width"])), "y": repr(y1), "width": style["colorscale-width"], "height": repr(y2 - y1), "style": PlotStyle.toString(gradientStyle)}))

                colorticklabelColor = style.get("colorticklabel-color", style["ticklabel-color"])
                colortickColor = style.get("colortick-color", style["tick-color"])
                colortickLength = float(style.get("colortick-length", style["tick-length"]))
                colorminitickLength = float(style.get("colorminitick-length", style["minitick-length"]))

                eps = defs.EPSILON * (bottomEdge - topEdge)
                transformedTicks = dict((colorticksSource(1.0, y)[1], label) for y, label in colorticks.items())
                transformedMiniticks = [colorticksSource(1.0, y)[1] for y in colorminiticks if y not in colorticks]
                colorticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (cx2, y, cx2 - colorminitickLength, y) for y in transformedMiniticks if y1 + eps < y < y2 - eps), style=("stroke: %s; fill: none" % colortickColor)))
                colorticksGroup.append(svg.path(d=" ".join("M %r,%r L %r,%r" % (cx2, y, cx2 - colortickLength, y) for y in transformedTicks if y1 + eps < y < y2 - eps), style="stroke: %s; fill: none" % colortickColor))

                if colorticksDraw in ("parallel-labels", "perpendicular-labels"):
                    xoffset, yoffset = float(style["colortick-label-xoffset"]), float(style["colortick-label-yoffset"])
                    textStyle["fill"] = colorticklabelColor

                    for y, label in transformedTicks.items():
                        if y1 - eps < y < y2 + eps:
                            labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "dy": dyMiddle, "style": PlotStyle.toString(textStyle)}
                            if colorticksDraw == "parallel-labels":
                                labelAttributes["transform"] = "translate(%r,%r) rotate(90)" % (cx2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "middle"
                            elif colorticksDraw == "perpendicular-labels":
                                labelAttributes["transform"] = "translate(%r,%r)" % (cx2 + xoffset, y + yoffset)
                                labelAttributes["text-anchor"] = "start"
                            colorticksGroup.append(svg.text(label, **labelAttributes))

                content.append(colorticksGroup)

            # color label
            colorlabel = self.get("colorlabel", "")
            if colorlabel != "":
                labelMargin = float(style.get("colorlabel-margin", style["label-margin"]))
                textStyle["fill"] = style.get("colorlabel-color", style["label-color"])
                labelAttributes = {"transform": "translate(%r,%r) rotate(90)" % (rightEdge - labelMargin, (y1 + y2)/2.0), "text-anchor": "middle", defs.XML_SPACE: "preserve", "dy": dyMiddle, "font-size": style["font-size"], "style": PlotStyle.toString(textStyle)}
                content.append(svg.text(colorlabel, **labelAttributes))

            performanceTable.end("tickmarks")

            ### draw the bounding box

            if rectStyle["stroke"] != "none":
                content.append(svg.rect(**subAttrib))

        content.extend(aboveTicks)

        performanceTable.end("PlotWindow")
        return svg.g(*content, **attrib)
Exemple #27
0
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotGuideLines draw")

        output = svg.g()

        for directive in self.xpath(
                "pmml:PlotVerticalLines | pmml:PlotHorizontalLines | pmml:PlotLine"
        ):
            style = dict(self.styleDefaults)
            currentStyle = directive.get("style")
            if currentStyle is not None:
                style.update(PlotStyle.toDict(currentStyle))
            style["fill"] = "none"
            style = PlotStyle.toString(style)

            if directive.hasTag("PlotVerticalLines"):
                try:
                    x0 = plotCoordinates.xfieldType.stringToValue(
                        directive["x0"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid x0: %r" %
                                                   directive["x0"])

                spacing = float(directive["spacing"])
                low = plotCoordinates.innerX1
                high = plotCoordinates.innerX2

                up = list(
                    NP("arange", x0, high, spacing, dtype=NP.dtype(float)))
                down = list(
                    NP("arange",
                       x0 - spacing,
                       low,
                       -spacing,
                       dtype=NP.dtype(float)))

                for x in up + down:
                    x1, y1 = x, float("-inf")
                    X1, Y1 = plotCoordinates(x1, y1)
                    x2, y2 = x, float("inf")
                    X2, Y2 = plotCoordinates(x2, y2)

                    output.append(
                        svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2),
                                 style=style))

            elif directive.hasTag("PlotHorizontalLines"):
                try:
                    y0 = plotCoordinates.xfieldType.stringToValue(
                        directive["y0"])
                except ValueError:
                    raise defs.PmmlValidationError("Invalid y0: %r" %
                                                   directive["y0"])

                spacing = float(directive["spacing"])
                low = plotCoordinates.innerY1
                high = plotCoordinates.innerY2

                up = list(
                    NP("arange", y0, high, spacing, dtype=NP.dtype(float)))
                down = list(
                    NP("arange",
                       y0 - spacing,
                       low,
                       -spacing,
                       dtype=NP.dtype(float)))

                for y in up + down:
                    x1, y1 = float("-inf"), y
                    X1, Y1 = plotCoordinates(x1, y1)
                    x2, y2 = float("inf"), y
                    X2, Y2 = plotCoordinates(x2, y2)

                    output.append(
                        svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2),
                                 style=style))

            elif directive.hasTag("PlotLine"):
                try:
                    x1 = plotCoordinates.xfieldType.stringToValue(
                        directive["x1"])
                    y1 = plotCoordinates.xfieldType.stringToValue(
                        directive["y1"])
                    x2 = plotCoordinates.xfieldType.stringToValue(
                        directive["x2"])
                    y2 = plotCoordinates.xfieldType.stringToValue(
                        directive["y2"])
                except ValueError:
                    raise defs.PmmlValidationError(
                        "Invalid x1, y1, x2, or y2: %r %r %r %r" %
                        (directive["x1"], directive["y1"], directive["x2"],
                         directive["y2"]))

                X1, Y1 = plotCoordinates(x1, y1)
                X2, Y2 = plotCoordinates(x2, y2)

                output.append(
                    svg.path(d="M %r %r L %r %r" % (X1, Y1, X2, Y2),
                             style=style))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotGuideLines draw")

        return output
Exemple #28
0
    def makeMarker(svgIdMarker, marker, style, plotSvgMarker):
        """Construct a marker from a set of known shapes or an SVG
        pictogram.

        @type svgIdMarker: string
        @param svgIdMarker: SVG id for the new marker.
        @type marker: string
        @param marker: Name of the marker shape; must be one of PLOT-MARKER-TYPE.
        @type style: dict
        @param style: CSS style for the marker in dictionary form.
        @type plotSvgMarker: PmmlBinding or None
        @param plotSvgMarker: A PlotSvgMarker element, which either contains an inline SvgBinding or a fileName pointing to an external image.
        @rtype: SvgBinding
        @return: The marker image, appropriate for adding to a PlotDefinitions.
        """

        svg = SvgBinding.elementMaker

        style["stroke"] = style["marker-outline"]
        del style["marker-outline"]
        markerSize = float(style["marker-size"])
        del style["marker-size"]

        if marker == "circle":
            return svg.circle(id=svgIdMarker, cx="0", cy="0", r=repr(markerSize), style=PlotStyle.toString(style))

        elif marker == "square":
            p =  markerSize
            m = -markerSize
            return svg.path(id=svgIdMarker, d="M %r,%r L %r,%r L %r,%r L %r,%r z" % (m,m, p,m, p,p, m,p), style=PlotStyle.toString(style))

        elif marker == "diamond":
            p =  math.sqrt(2.0) * markerSize
            m = -math.sqrt(2.0) * markerSize
            return svg.path(id=svgIdMarker, d="M %r,0 L 0,%r L %r,0 L 0,%r z" % (m, m, p, p), style=PlotStyle.toString(style))

        elif marker == "plus":
            p =  markerSize
            m = -markerSize
            if style["stroke"] == "none":
                style["stroke"] = style["fill"]
            style["fill"] = "none"
            return svg.path(id=svgIdMarker, d="M %r,0 L %r,0 M 0,%r L 0,%r" % (m, p, m, p), style=PlotStyle.toString(style))

        elif marker == "times":
            p =  math.sqrt(2.0) * markerSize
            m = -math.sqrt(2.0) * markerSize
            if style["stroke"] == "none":
                style["stroke"] = style["fill"]
            style["fill"] = "none"
            return svg.path(id=svgIdMarker, d="M %r,%r L %r,%r M %r,%r L %r,%r" % (m,m, p,p, p,m, m,p), style=PlotStyle.toString(style))

        elif marker == "svg":
            if plotSvgMarker is None:
                raise defs.PmmlValidationError("When marker is \"svg\", a PlotSvgMarker must be provided")

            inlineSvg = plotSvgMarker.getchildren()
            fileName = plotSvgMarker.get("fileName")
            if len(inlineSvg) == 1 and fileName is None:
                svgBinding = inlineSvg[0]
            elif len(inlineSvg) == 0 and fileName is not None:
                svgBinding = SvgBinding.loadXml(fileName)
            else:
                raise defs.PmmlValidationError("PlotSvgMarker should specify an inline SVG or a fileName but not both or neither")

            sx1, sy1, sx2, sy2 = PlotSvgAnnotation.findSize(svgBinding)
            tx1, ty1 = -markerSize, -markerSize
            tx2, ty2 = markerSize, markerSize

            transform = "translate(%r, %r) scale(%r, %r)" % (tx1 - sx1, ty1 - sy1, (tx2 - tx1)/float(sx2 - sx1), (ty2 - ty1)/float(sy2 - sy1))
            return svg.g(copy.deepcopy(svgBinding), id=svgIdMarker, transform=transform)
Exemple #29
0
class PlotLegendNumber(PmmlPlotLegendContent):
    """PlotLegendNumber is an element that can be placed in a
    PlotLegend to present a mutable number.
    
    PMML content:

      - Text representation of the initial value.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - digits: number of significant digits to present.
      - style: CSS style properties.

    CSS properties:

      - font, font-family, font-size, font-size-adjust, font-stretch,
        font-style, font-variant, font-weight: font properties.
      - text-color: text color.
                       
    @type value: number
    @param value: Programmatic access to the value.
    """

    styleProperties = [
        "font",
        "font-family",
        "font-size",
        "font-size-adjust",
        "font-stretch",
        "font-style",
        "font-variant",
        "font-weight",
        "text-color",
    ]

    styleDefaults = {"font-size": "25.0", "text-color": "black"}

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotLegendNumber">
        <xs:complexType>
            <xs:simpleContent>
                <xs:extension base="xs:double">
                    <xs:attribute name="svgId" type="xs:string" use="optional" />
                    <xs:attribute name="digits" type="xs:nonNegativeInteger" use="optional" />
                    <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
                </xs:extension>
            </xs:simpleContent>
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    @property
    def value(self):
        try:
            float(self.text)
        except (ValueError, TypeError):
            self.text = "0"
        return float(self.text)

    @value.setter
    def value(self, value):
        self.text = repr(value)

    def draw(self, dataTable, functionTable, performanceTable, rowIndex,
             colIndex, cellContents, labelAttributes, plotDefinitions):
        """Draw the plot legend content, which is more often text than graphics.

        @type dataTable: DataTable
        @param dataTable: Contains the data to describe, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type rowIndex: int
        @param rowIndex: Row number of the C{cellContents} to fill.
        @type colIndex: int
        @param colIndex: Column number of the C{cellContents} to fill.
        @type cellContents: dict
        @param cellContents: Dictionary that maps pairs of integers to SVG graphics to draw.
        @type labelAttributes: CSS style dict
        @param labelAttributes: Style properties that are defined at the level of the legend and must percolate down to all drawables within the legend.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: 2-tuple
        @return: The next C{rowIndex} and C{colIndex} in the sequence.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLegendNumber")

        myLabelAttributes = dict(labelAttributes)
        style = PlotStyle.toDict(myLabelAttributes["style"])
        style.update(self.getStyleState())
        myLabelAttributes["style"] = PlotStyle.toString(style)
        myLabelAttributes["font-size"] = style["font-size"]

        svgId = self.get("svgId")
        if svgId is not None:
            myLabelAttributes["id"] = svgId

        try:
            float(self.text)
        except (ValueError, TypeError):
            self.text = "0"

        digits = self.get("digits")
        if digits is not None:
            astext = PlotNumberFormat.roundDigits(float(self.text),
                                                  int(digits))
        else:
            astext = PlotNumberFormat.toUnicode(self.text)

        cellContents[rowIndex, colIndex] = svg.text(astext,
                                                    **myLabelAttributes)
        colIndex += 1

        performanceTable.end("PlotLegendNumber")
        return rowIndex, colIndex
Exemple #30
0
    def frame(self, dataTable, functionTable, performanceTable,
              plotCoordinates, plotContentBox, plotDefinitions):
        """Draw a plot frame and the plot elements it contains.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed (not the coordinate system defined by the plot).
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLayout")

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        style = self.getStyleState()

        title = self.get("title")
        if title is not None:
            textStyle = {"stroke": "none", "fill": style["title-color"]}
            for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
                if styleProperty in style:
                    textStyle[styleProperty] = style[styleProperty]

            plotContentBox = plotContentBox.subContent({
                "margin-top":
                repr(
                    float(style.get("margin-top", style["margin"])) +
                    float(style["title-height"]) + float(style["title-gap"]))
            })
            content.append(
                svg.text(
                    title, **{
                        "transform":
                        "translate(%r,%r)" %
                        (plotContentBox.x + plotContentBox.width / 2.0,
                         plotContentBox.y - float(style["title-gap"])),
                        "text-anchor":
                        "middle",
                        defs.XML_SPACE:
                        "preserve",
                        "style":
                        PlotStyle.toString(textStyle)
                    }))

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        ### background rectangle
        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {
                "x": repr(x1),
                "y": repr(y1),
                "width": repr(x2 - x1),
                "height": repr(y2 - y1),
                "style": PlotStyle.toString(rectStyle)
            }

            if rectStyle["fill"] != "none":
                if "background-opacity" in style:
                    rectStyle["fill-opacity"] = style["background-opacity"]
                if svgId is not None:
                    subAttrib["id"] = svgId + ".background"
                content.append(svg.rect(**subAttrib))

        ### sub-content
        if subContentBox is not None:
            plotFrames = self.childrenOfClass(PmmlPlotFrame)
            rows = self.get("rows", defaultFromXsd=True, convertType=True)
            cols = self.get("cols", defaultFromXsd=True, convertType=True)

            rowHeights = style["row-heights"]
            if rowHeights == "auto":
                rowHeights = [subContentBox.height / float(rows)] * rows
            else:
                try:
                    rowHeights = map(float, rowHeights.split())
                    if any(x <= 0.0 for x in rowHeights): raise ValueError
                except ValueError:
                    raise defs.PmmlValidationError(
                        "If not \"auto\", all items in row-heights must be positive numbers"
                    )
                if len(rowHeights) != rows:
                    raise defs.PmmlValidationError(
                        "Number of elements in row-heights (%d) must be equal to rows (%d)"
                        % (len(rowHeights), rows))

                norm = sum(rowHeights) / subContentBox.height
                rowHeights = [x / norm for x in rowHeights]

            colWidths = style["col-widths"]
            if colWidths == "auto":
                colWidths = [subContentBox.width / float(cols)] * cols
            else:
                try:
                    colWidths = map(float, colWidths.split())
                    if any(x <= 0.0 for x in colWidths): raise ValueError
                except ValueError:
                    raise defs.PmmlValidationError(
                        "If not \"auto\", all items in col-widths must be positive numbers"
                    )
                if len(colWidths) != cols:
                    raise defs.PmmlValidationError(
                        "Number of elements in col-widths (%d) must be equal to cols (%d)"
                        % (len(colWidths), cols))

                norm = sum(colWidths) / subContentBox.width
                colWidths = [x / norm for x in colWidths]

            plotFramesIndex = 0
            cellY = subContentBox.y
            for vertCell in xrange(rows):
                cellX = subContentBox.x
                for horizCell in xrange(cols):
                    if plotFramesIndex < len(plotFrames):
                        plotFrame = plotFrames[plotFramesIndex]

                        cellCoordinates = PlotCoordinatesOffset(
                            plotCoordinates, cellX, cellY)
                        cellContentBox = PlotContentBox(
                            0, 0, colWidths[horizCell], rowHeights[vertCell])

                        performanceTable.pause("PlotLayout")
                        content.append(
                            plotFrame.frame(dataTable, functionTable,
                                            performanceTable, cellCoordinates,
                                            cellContentBox, plotDefinitions))
                        performanceTable.unpause("PlotLayout")

                    plotFramesIndex += 1
                    cellX += colWidths[horizCell]
                cellY += rowHeights[vertCell]

        ### border rectangle (reuses subAttrib, replaces subAttrib["style"])
        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace(
                            "border-", "stroke-")] = style[styleProperty]

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        performanceTable.end("PlotLayout")
        return svg.g(*content, **attrib)
Exemple #31
0
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotHistogram draw")

        cumulative = self.get("cumulative", defaultFromXsd=True, convertType=True)
        vertical = self.get("vertical", defaultFromXsd=True, convertType=True)
        visualization = self.get("visualization", defaultFromXsd=True)

        output = svg.g()
        if len(state.count) > 0:
            if state.fieldType is not self.fieldTypeNumeric:
                if vertical:
                    strings = plotCoordinates.xstrings
                else:
                    strings = plotCoordinates.ystrings

                newCount = []
                for string in strings:
                    try:
                        index = state.edges.index(string)
                    except ValueError:
                        newCount.append(0.0)
                    else:
                        newCount.append(state.count[index])

                state.count = newCount
                state.edges = [(i - 0.5, i + 0.5) for i in xrange(len(strings))]

            if vertical:
                Ax = NP("array", [low if low is not None else float("-inf") for low, high in state.edges], dtype=NP.dtype(float))
                Bx = NP(Ax.copy())
                Cx = NP("array", [high if high is not None else float("inf") for low, high in state.edges], dtype=NP.dtype(float))
                Dx = NP(Cx.copy())
                Ay = NP("zeros", len(state.count), dtype=NP.dtype(float))
                if cumulative:
                    Cy = NP("cumsum", NP("array", state.count, dtype=NP.dtype(float)))
                    By = NP("roll", Cy, 1)
                    By[0] = 0.0
                else:
                    By = NP("array", state.count, dtype=NP.dtype(float))
                    Cy = NP(By.copy())
                Dy = NP(Ay.copy())

            else:
                if cumulative:
                    Cx = NP("cumsum", NP("array", state.count, dtype=NP.dtype(float)))
                    Bx = NP("roll", Cx, 1)
                    Bx[0] = 0.0
                else:
                    Bx = NP("array", state.count, dtype=NP.dtype(float))
                    Cx = NP(Bx.copy())
                Ax = NP("zeros", len(state.count), dtype=NP.dtype(float))
                Dx = NP(Ax.copy())
                Ay = NP("array", [low if low is not None else float("-inf") for low, high in state.edges], dtype=NP.dtype(float))
                By = NP(Ay.copy())
                Cy = NP("array", [high if high is not None else float("inf") for low, high in state.edges], dtype=NP.dtype(float))
                Dy = NP(Cy.copy())

            AX, AY = plotCoordinates(Ax, Ay)
            BX, BY = plotCoordinates(Bx, By)
            CX, CY = plotCoordinates(Cx, Cy)
            DX, DY = plotCoordinates(Dx, Dy)

            if visualization == "skyline":
                gap = self.get("gap", defaultFromXsd=True, convertType=True)

                if vertical:
                    if gap > 0.0 and NP(NP(DX - gap/2.0) - NP(AX + gap/2.0)).min() > 0.0:
                        AX += gap/2.0
                        BX += gap/2.0
                        CX -= gap/2.0
                        DX -= gap/2.0
                else:
                    if gap > 0.0 and NP(NP(AY + gap/2.0) - NP(DY - gap/2.0)).min() > 0.0:
                        AY -= gap/2.0
                        BY -= gap/2.0
                        CY += gap/2.0
                        DY += gap/2.0

                pathdata = []
                nextIsMoveto = True
                for i in xrange(len(state.count)):
                    iprev = i - 1
                    inext = i + 1

                    if vertical and By[i] == 0.0 and Cy[i] == 0.0:
                        if i > 0 and not nextIsMoveto:
                            pathdata.append("L %r %r" % (DX[iprev], DY[iprev]))
                        nextIsMoveto = True

                    elif not vertical and Bx[i] == 0.0 and Cx[i] == 0.0:
                        if i > 0 and not nextIsMoveto:
                            pathdata.append("L %r %r" % (DX[iprev], DY[iprev]))
                        nextIsMoveto = True

                    else:
                        if nextIsMoveto or gap > 0.0 or (vertical and DX[iprev] != AX[i]) or (not vertical and DY[iprev] != AY[i]):
                            pathdata.append("M %r %r" % (AX[i], AY[i]))
                            nextIsMoveto = False

                        pathdata.append("L %r %r" % (BX[i], BY[i]))
                        pathdata.append("L %r %r" % (CX[i], CY[i]))

                        if i == len(state.count) - 1 or gap > 0.0 or (vertical and DX[i] != AX[inext]) or (not vertical and DY[i] != AY[inext]):
                            pathdata.append("L %r %r" % (DX[i], DY[i]))

                style = self.getStyleState()
                del style["marker-size"]
                del style["marker-outline"]
                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(style)))

            elif visualization == "polyline":
                pathdata = []
                for i in xrange(len(state.count)):
                    if i == 0:
                        pathdata.append("M %r %r" % (AX[i], AY[i]))

                    pathdata.append("L %r %r" % ((BX[i] + CX[i])/2.0, (BY[i] + CY[i])/2.0))

                    if i == len(state.count) - 1:
                        pathdata.append("L %r %r" % (DX[i], DY[i]))

                style = self.getStyleState()
                del style["marker-size"]
                del style["marker-outline"]
                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(style)))

            elif visualization == "smooth":
                smoothingSamples = math.ceil(len(state.count) / 2.0)

                BCX = NP(NP(BX + CX) / 2.0)
                BCY = NP(NP(BY + CY) / 2.0)

                xarray = NP("array", [AX[0]] + list(BCX) + [DX[-1]], dtype=NP.dtype(float))
                yarray = NP("array", [AY[0]] + list(BCY) + [DY[-1]], dtype=NP.dtype(float))
                samples = NP("linspace", AX[0], DX[-1], int(smoothingSamples), endpoint=True)
                smoothingScale = abs(DX[-1] - AX[0]) / smoothingSamples

                xlist, ylist, dxlist, dylist = PlotCurve.pointsToSmoothCurve(xarray, yarray, samples, smoothingScale, False)

                pathdata = PlotCurve.formatPathdata(xlist, ylist, dxlist, dylist, PlotCoordinates(), False, True)

                style = self.getStyleState()
                fillStyle = dict((x, style[x]) for x in style if x.startswith("fill"))
                fillStyle["stroke"] = "none"
                strokeStyle = dict((x, style[x]) for x in style if x.startswith("stroke"))

                if style["fill"] != "none" and len(pathdata) > 0:
                    if vertical:
                        firstPoint = plotCoordinates(Ax[0], 0.0)
                        lastPoint = plotCoordinates(Dx[-1], 0.0)
                    else:
                        firstPoint = plotCoordinates(0.0, Ay[0])
                        lastPoint = plotCoordinates(0.0, Dy[-1])
                        
                    pathdata2 = ["M %r %r" % firstPoint, pathdata[0].replace("M", "L")]
                    pathdata2.extend(pathdata[1:])
                    pathdata2.append(pathdata[-1])
                    pathdata2.append("L %r %r" % lastPoint)

                    output.append(svg.path(d=" ".join(pathdata2), style=PlotStyle.toString(fillStyle)))

                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(strokeStyle)))

            elif visualization == "points":
                currentStyle = PlotStyle.toDict(self.get("style") or {})
                style = self.getStyleState()
                if "fill" not in currentStyle:
                    style["fill"] = "black"

                BCX = NP(NP(BX + CX) / 2.0)
                BCY = NP(NP(BY + CY) / 2.0)

                svgId = self.get("svgId")
                if svgId is None:
                    svgIdMarker = plotDefinitions.uniqueName()
                else:
                    svgIdMarker = svgId + ".marker"

                marker = PlotScatter.makeMarker(svgIdMarker, self.get("marker", defaultFromXsd=True), style, self.childOfTag("PlotSvgMarker"))
                plotDefinitions[marker.get("id")] = marker

                markerReference = "#" + marker.get("id")
                output.extend(svg.use(**{"x": repr(x), "y": repr(y), defs.XLINK_HREF: markerReference}) for x, y in itertools.izip(BCX, BCY))
                
            else:
                raise NotImplementedError("TODO: add 'errorbars'")

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotHistogram draw")
        return output
Exemple #32
0
    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotCurve draw")

        loop = self.get("loop", defaultFromXsd=True, convertType=True)
        pathdata = self.formatPathdata(
            state.x, state.y, state.dx, state.dy, plotCoordinates, loop,
            (state.dx is not None and state.dy is not None))
        output = svg.g()

        style = self.getStyleState()
        strokeStyle = dict(
            (x, style[x]) for x in style if x.startswith("stroke"))
        fillStyle = dict((x, style[x]) for x in style if x.startswith("fill"))
        fillStyle["stroke"] = "none"

        if style["fill"] != "none":
            if len(self.xpath("pmml:PlotFormula[@role='y(x)']")) > 0 and len(
                    pathdata) > 1:
                firstPoint = plotCoordinates(state.x[0], 0.0)
                lastPoint = plotCoordinates(state.x[-1], 0.0)

                X0, Y0 = plotCoordinates(state.x[0], state.y[0])

                pathdata2 = ["M %r %r" % firstPoint]
                pathdata2.append("L %r %r" % (X0, Y0))
                pathdata2.extend(pathdata[1:])
                pathdata2.append("L %r %r" % lastPoint)

                output.append(
                    svg.path(d=" ".join(pathdata2),
                             style=PlotStyle.toString(fillStyle)))

            else:
                output.append(
                    svg.path(d=" ".join(pathdata),
                             style=PlotStyle.toString(fillStyle)))

        output.append(
            svg.path(d=" ".join(pathdata),
                     style=PlotStyle.toString(strokeStyle)))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotCurve draw")
        return output
Exemple #33
0
    def draw(self, dataTable, functionTable, performanceTable, plotCoordinates,
             plotContentBox, plotDefinitions):
        """Draw the plot annotation.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed.
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLegend")

        # figure out how to format text
        style = self.getStyleState()
        textStyle = {"fill": style["text-color"], "stroke": "none"}
        for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
            if styleProperty in style:
                textStyle[styleProperty] = style[styleProperty]
        labelAttributes = {
            "font-size": style["font-size"],
            defs.XML_SPACE: "preserve",
            "style": PlotStyle.toString(textStyle)
        }

        columnAlign = style["column-align"]
        if not set(columnAlign.lower()).issubset(set(["l", "m", "r", "."])):
            raise defs.PmmlValidationError(
                "PlotLegend's column-align style property may only contain the following characters: \"l\", \"m\", \"r\", \".\""
            )

        columnPadding = float(style["column-padding"])

        ### get an <svg:text> object for each cell

        # content follows the same delimiter logic as Array, except that lineseps outside of quotes signify new table rows
        rowIndex = 0
        colIndex = 0
        cellContents = {}
        for item in sum([[x, x.tail]
                         for x in self.childrenOfClass(PmmlPlotLegendContent)],
                        [self.text]):
            if item is None: pass

            elif isinstance(item, basestring):
                for word in re.finditer(self._re_word, item):
                    one, two, three = word.groups()

                    # quoted text; take it all as-is, without the outermost quotes and unquoting quoted quotes
                    if two is not None:
                        cellContents[rowIndex, colIndex] = svg.text(
                            two.replace(r'\"', '"'), **labelAttributes)
                        colIndex += 1

                    elif one == r'""':
                        colIndex += 1

                    else:
                        newlineIndex = one.find(os.linesep)
                        if newlineIndex == 0 and not (rowIndex == 0
                                                      and colIndex == 0):
                            rowIndex += 1
                            colIndex = 0
                        while newlineIndex != -1:
                            if one[:newlineIndex] != "":
                                cellContents[rowIndex, colIndex] = svg.text(
                                    one[:newlineIndex], **labelAttributes)
                                rowIndex += 1
                                colIndex = 0

                            one = one[(newlineIndex + len(os.linesep)):]
                            newlineIndex = one.find(os.linesep)

                        if one != "":
                            cellContents[rowIndex, colIndex] = svg.text(
                                one, **labelAttributes)
                            colIndex += 1

            else:
                performanceTable.pause("PlotLegend")
                rowIndex, colIndex = item.draw(dataTable, functionTable,
                                               performanceTable, rowIndex,
                                               colIndex, cellContents,
                                               labelAttributes,
                                               plotDefinitions)
                performanceTable.unpause("PlotLegend")

        maxRows = 0
        maxCols = 0
        maxChars = {}
        beforeDot = {}
        afterDot = {}
        for row, col in cellContents:
            if row > maxRows:
                maxRows = row
            if col > maxCols:
                maxCols = col

            if col >= len(columnAlign):
                alignment = columnAlign[-1]
            else:
                alignment = columnAlign[col]

            if col not in maxChars:
                maxChars[col] = 0
                beforeDot[col] = 0
                afterDot[col] = 0

            textContent = cellContents[row, col].text
            if textContent is not None:
                if len(textContent) > maxChars[col]:
                    maxChars[col] = len(textContent)

                if alignment == ".":
                    dotPosition = textContent.find(".")
                    if dotPosition == -1:
                        dotPosition = textContent.find("e")
                        if dotPosition == -1:
                            dotPosition = textContent.find("E")
                            if dotPosition == -1:
                                dotPosition = textContent.find(u"\u00d710")
                                if dotPosition == -1:
                                    dotPosition = len(textContent)
                    if dotPosition > beforeDot[col]:
                        beforeDot[col] = dotPosition
                    if len(textContent) - dotPosition > afterDot[col]:
                        afterDot[col] = len(textContent) - dotPosition

        maxRows += 1
        maxCols += 1
        for col in xrange(maxCols):
            if beforeDot[col] + afterDot[col] > maxChars[col]:
                maxChars[col] = beforeDot[col] + afterDot[col]
        cellWidthDenom = float(sum(maxChars.values()))

        ### create a subContentBox and fill the table cells

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        # change some of the margins based on text, unless overridden by explicit styleProperties

        if style.get("margin-bottom") == "auto": del style["margin-bottom"]
        if style.get("margin-top") == "auto": del style["margin-top"]
        if style.get("margin-left") == "auto": del style["margin-left"]
        if style.get("margin-right") == "auto": del style["margin-right"]

        subContentBox = plotContentBox.subContent(style)
        nominalHeight = maxRows * float(style["font-size"])
        nominalWidth = cellWidthDenom * 0.5 * float(
            style["font-size"]) + columnPadding * (maxCols - 1)

        if nominalHeight < subContentBox.height:
            if "margin-bottom" in style and "margin-top" in style:
                pass
            elif "margin-bottom" in style:
                style["margin-top"] = subContentBox.height - nominalHeight
            elif "margin-top" in style:
                style["margin-bottom"] = subContentBox.height - nominalHeight
            else:
                style["margin-bottom"] = style["margin-top"] = (
                    subContentBox.height - nominalHeight) / 2.0

        if nominalWidth < subContentBox.width:
            if "margin-left" in style and "margin-right" in style:
                pass
            elif "margin-left" in style:
                style["margin-right"] = subContentBox.width - nominalWidth
            elif "margin-right" in style:
                style["margin-left"] = subContentBox.width - nominalWidth
            else:
                style["margin-left"] = style["margin-right"] = (
                    subContentBox.width - nominalWidth) / 2.0

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        ### create a border rectangle
        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            if "background-opacity" in style:
                rectStyle["fill-opacity"] = style["background-opacity"]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {
                "x": repr(x1),
                "y": repr(y1),
                "width": repr(x2 - x1),
                "height": repr(y2 - y1),
                "style": PlotStyle.toString(rectStyle)
            }
            if svgId is not None:
                subAttrib["id"] = svgId + ".background"

            if rectStyle["fill"] != "none":
                content.append(svg.rect(**subAttrib))

        ### put the cell content in the table
        if subContentBox is not None:
            cellHeight = subContentBox.height / float(maxRows)
            colStart = [subContentBox.x]
            for col in xrange(maxCols):
                colStart.append(colStart[col] + subContentBox.width *
                                maxChars[col] / cellWidthDenom)

            for row in xrange(maxRows):
                for col in xrange(maxCols):
                    cellContent = cellContents.get((row, col))
                    if cellContent is not None:
                        if col >= len(columnAlign):
                            alignment = columnAlign[-1]
                        else:
                            alignment = columnAlign[col]

                        textContent = None
                        if cellContent.tag == "text" or cellContent.tag[
                                -5:] == "}text":
                            if alignment.lower() == "l":
                                cellContent.set("text-anchor", "start")
                            elif alignment.lower() == "m":
                                cellContent.set("text-anchor", "middle")
                            elif alignment.lower() == "r":
                                cellContent.set("text-anchor", "end")
                            elif alignment.lower() == ".":
                                cellContent.set("text-anchor", "middle")
                            textContent = cellContent.text

                        if alignment.lower() == ".":
                            if textContent is None:
                                alignment = "m"
                            else:
                                dotPosition = textContent.find(".")
                                if dotPosition == -1:
                                    dotPosition = textContent.find("e")
                                    if dotPosition == -1:
                                        dotPosition = textContent.find("E")
                                        if dotPosition == -1:
                                            dotPosition = textContent.find(
                                                u"\u00d710")
                                            if dotPosition == -1:
                                                dotPosition = len(
                                                    textContent) - 0.3
                                dotPosition += 0.2 * textContent[:int(
                                    math.ceil(dotPosition))].count(u"\u2212")

                                x = (colStart[col] + colStart[col + 1]) / 2.0
                                x -= (dotPosition - 0.5 * len(textContent) +
                                      0.5) * nominalWidth / cellWidthDenom

                        if alignment.lower() == "l":
                            x = colStart[col]
                        elif alignment.lower() == "m":
                            x = (colStart[col] + colStart[col + 1]) / 2.0
                        elif alignment.lower() == "r":
                            x = colStart[col + 1]

                        y = subContentBox.y + cellHeight * (row + 0.75)
                        x, y = plotCoordinates(x, y)

                        cellContent.set("transform",
                                        "translate(%r,%r)" % (x, y))
                        content.append(cellContent)

        ### create a border rectangle (reuses subAttrib, replaces subAttrib["style"])
        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace(
                            "border-", "stroke-")] = style[styleProperty]

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        performanceTable.end("PlotLegend")
        return svg.g(*content, **attrib)
Exemple #34
0
 def style(self, value):
     self.set("style", PlotStyle.toString(value))
Exemple #35
0
class PlotScatter(PmmlPlotContent):
    """PlotScatter represents a scatter plot of x-y values defined by
    two expressions.

    PMML subelements:

      - PlotNumericExpression role="x"
      - PlotNumericExpression role="y"
      - PlotNumericExpression role="x-errorbar"
      - PlotNumericExpression role="x-errorbar-up"
      - PlotNumericExpression role="x-errorbar-down"
      - PlotNumericExpression role="y-errorbar"
      - PlotNumericExpression role="y-errorbar-up"
      - PlotNumericExpression role="y-errorbar-down"
      - PlotNumericExpression role="weight"
      - PlotSelection: expression or predicate to filter the data before plotting.

    Errorbars do not need to be specified, but asymmetric and
    symmetric error bars are mututally exclusive.

    The optional C{weight} scales the opacity according to values
    observed in data.  These must be scaled by the user to lie in the
    range 0 to 1.

    PMML attributes:

      - svgId: id for the resulting SVG element.
      - stateId: key for persistent storage in a DataTableState.
      - marker: type of marker, must be one of PLOT-MARKER-TYPE.
      - limit: optional number specifying the maximum number of data
        points to generate.  If the true number of data points exceeds
        this limit, points will be randomly chosen.
      - style: CSS style properties.

    CSS properties:

      - fill, fill-opacity: color of the markers.
      - stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap,
        stroke-linejoin, stroke-miterlimit, stroke-opacity,
        stroke-width: properties of the marker lines and error bar
        lines.
      - marker-size: size of the marker.
      - marker-outline: optional outline for the marker.

    See the source code for the full XSD.
    """

    styleProperties = ["fill", "fill-opacity", 
                       "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width",
                       "marker-size", "marker-outline",
                       ]

    styleDefaults = {"fill": "black", "stroke": "black", "marker-size": "5", "marker-outline": "none"}

    xsd = """<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="PlotScatter">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="Extension" minOccurs="0" maxOccurs="unbounded" />
                <xs:element ref="PlotNumericExpression" minOccurs="2" maxOccurs="9" />
                <xs:element ref="PlotSelection" minOccurs="0" maxOccurs="1" />
                <xs:element ref="PlotSvgMarker" minOccurs="0" maxOccurs="1" />
            </xs:sequence>
            <xs:attribute name="svgId" type="xs:string" use="optional" />
            <xs:attribute name="stateId" type="xs:string" use="optional" />
            <xs:attribute name="marker" type="PLOT-MARKER-TYPE" use="optional" default="circle" />
            <xs:attribute name="limit" type="INT-NUMBER" use="optional" />
            <xs:attribute name="style" type="xs:string" use="optional" default="%s" />
        </xs:complexType>
    </xs:element>
</xs:schema>
""" % PlotStyle.toString(styleDefaults)

    xsdRemove = ["PLOT-MARKER-TYPE", "PlotSvgMarker"]

    xsdAppend = ["""<xs:simpleType name="PLOT-MARKER-TYPE" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:restriction base="xs:string">
        <xs:enumeration value="circle" />
        <xs:enumeration value="square" />
        <xs:enumeration value="diamond" />
        <xs:enumeration value="plus" />
        <xs:enumeration value="times" />
        <xs:enumeration value="svg" />
    </xs:restriction>
</xs:simpleType>
""",
                 """<xs:element name="PlotSvgMarker" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:complexType>
        <xs:complexContent>
            <xs:restriction base="xs:anyType">
                <xs:sequence>
                    <xs:any minOccurs="0" maxOccurs="1" processContents="skip" />
                </xs:sequence>
                <xs:attribute name="fileName" type="xs:string" use="optional" />
            </xs:restriction>
        </xs:complexContent>
    </xs:complexType>
</xs:element>
"""]

    @staticmethod
    def makeMarker(svgIdMarker, marker, style, plotSvgMarker):
        """Construct a marker from a set of known shapes or an SVG
        pictogram.

        @type svgIdMarker: string
        @param svgIdMarker: SVG id for the new marker.
        @type marker: string
        @param marker: Name of the marker shape; must be one of PLOT-MARKER-TYPE.
        @type style: dict
        @param style: CSS style for the marker in dictionary form.
        @type plotSvgMarker: PmmlBinding or None
        @param plotSvgMarker: A PlotSvgMarker element, which either contains an inline SvgBinding or a fileName pointing to an external image.
        @rtype: SvgBinding
        @return: The marker image, appropriate for adding to a PlotDefinitions.
        """

        svg = SvgBinding.elementMaker

        style["stroke"] = style["marker-outline"]
        del style["marker-outline"]
        markerSize = float(style["marker-size"])
        del style["marker-size"]

        if marker == "circle":
            return svg.circle(id=svgIdMarker, cx="0", cy="0", r=repr(markerSize), style=PlotStyle.toString(style))

        elif marker == "square":
            p =  markerSize
            m = -markerSize
            return svg.path(id=svgIdMarker, d="M %r,%r L %r,%r L %r,%r L %r,%r z" % (m,m, p,m, p,p, m,p), style=PlotStyle.toString(style))

        elif marker == "diamond":
            p =  math.sqrt(2.0) * markerSize
            m = -math.sqrt(2.0) * markerSize
            return svg.path(id=svgIdMarker, d="M %r,0 L 0,%r L %r,0 L 0,%r z" % (m, m, p, p), style=PlotStyle.toString(style))

        elif marker == "plus":
            p =  markerSize
            m = -markerSize
            if style["stroke"] == "none":
                style["stroke"] = style["fill"]
            style["fill"] = "none"
            return svg.path(id=svgIdMarker, d="M %r,0 L %r,0 M 0,%r L 0,%r" % (m, p, m, p), style=PlotStyle.toString(style))

        elif marker == "times":
            p =  math.sqrt(2.0) * markerSize
            m = -math.sqrt(2.0) * markerSize
            if style["stroke"] == "none":
                style["stroke"] = style["fill"]
            style["fill"] = "none"
            return svg.path(id=svgIdMarker, d="M %r,%r L %r,%r M %r,%r L %r,%r" % (m,m, p,p, p,m, m,p), style=PlotStyle.toString(style))

        elif marker == "svg":
            if plotSvgMarker is None:
                raise defs.PmmlValidationError("When marker is \"svg\", a PlotSvgMarker must be provided")

            inlineSvg = plotSvgMarker.getchildren()
            fileName = plotSvgMarker.get("fileName")
            if len(inlineSvg) == 1 and fileName is None:
                svgBinding = inlineSvg[0]
            elif len(inlineSvg) == 0 and fileName is not None:
                svgBinding = SvgBinding.loadXml(fileName)
            else:
                raise defs.PmmlValidationError("PlotSvgMarker should specify an inline SVG or a fileName but not both or neither")

            sx1, sy1, sx2, sy2 = PlotSvgAnnotation.findSize(svgBinding)
            tx1, ty1 = -markerSize, -markerSize
            tx2, ty2 = markerSize, markerSize

            transform = "translate(%r, %r) scale(%r, %r)" % (tx1 - sx1, ty1 - sy1, (tx2 - tx1)/float(sx2 - sx1), (ty2 - ty1)/float(sy2 - sy1))
            return svg.g(copy.deepcopy(svgBinding), id=svgIdMarker, transform=transform)

    @staticmethod
    def drawErrorbars(xarray, yarray, exup, exdown, eyup, eydown, markerSize, strokeStyle, weight=None):
        """Draw a set of error bars, given values in global SVG
        coordinates.

        @type xarray: 1d Numpy array
        @param xarray: The X positions in global SVG coordinates.
        @type yarray: 1d Numpy array
        @param yarray: The Y positions in global SVG coordinates.
        @type exup: 1d Numpy array or None
        @param exup: The upper ends of the X error bars in global SVG coordinates (already added to the X positions).
        @type exdown: 1d Numpy array or None
        @param exdown: The lower ends of the X error bars in global SVG coordinates (already added to the X positions).
        @type eyup: 1d Numpy array or None
        @param eyup: The upper ends of the Y error bars in global SVG coordinates (already added to the Y positions).
        @type eydown: 1d Numpy array or None
        @param eydown: The lower ends of the Y error bars in global SVG coordinates (already added to the Y positions).
        @type markerSize: number
        @param markerSize: Size of the marker in SVG coordinates.
        @type strokeStyle: dict
        @param strokeStyle: CSS style attributes appropriate for stroking (not filling) in dictionary form.
        @type weight: 1d Numpy array or None
        @param weight: The opacity of each point (if None, the opacity is not specified and is therefore fully opaque).
        """

        svg = SvgBinding.elementMaker
        output = []
        
        strokeStyle = copy.copy(strokeStyle)
        strokeStyle["fill"] = "none"
        if weight is not None:
            strokeStyle["opacity"] = "1"

        for i in xrange(len(xarray)):
            x = xarray[i]
            y = yarray[i]

            pathdata = []

            if exup is not None:
                pathdata.append("M %r %r L %r %r" % (exdown[i], y             ,   exup[i], y             ))
                pathdata.append("M %r %r L %r %r" % (exdown[i], y - markerSize, exdown[i], y + markerSize))
                pathdata.append("M %r %r L %r %r" % (  exup[i], y - markerSize,   exup[i], y + markerSize))

            if eyup is not None:
                pathdata.append("M %r %r L %r %r" % (x             , eydown[i], x             ,   eyup[i]))
                pathdata.append("M %r %r L %r %r" % (x - markerSize, eydown[i], x + markerSize, eydown[i]))
                pathdata.append("M %r %r L %r %r" % (x - markerSize, eyup[i],   x + markerSize,   eyup[i]))

            if len(pathdata) > 0:
                if weight is not None:
                    strokeStyle["opacity"] = repr(weight[i])
                output.append(svg.path(d=" ".join(pathdata), style=PlotStyle.toString(strokeStyle)))

        return output

    def _makeMarker(self, plotDefinitions):
        """Used by C{draw}."""

        style = self.getStyleState()

        svgId = self.get("svgId")
        if svgId is None:
            svgIdMarker = plotDefinitions.uniqueName()
        else:
            svgIdMarker = svgId + ".marker"

        marker = self.get("marker", defaultFromXsd=True)

        return self.makeMarker(svgIdMarker, marker, style, self.childOfTag("PlotSvgMarker"))

    def prepare(self, state, dataTable, functionTable, performanceTable, plotRange):
        """Prepare a plot element for drawing.

        This stage consists of calculating all quantities and
        determing the bounds of the data.  These bounds may be unioned
        with bounds from other plot elements that overlay this plot
        element, so the drawing (which requires a finalized coordinate
        system) cannot begin yet.

        This method modifies C{plotRange}.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type dataTable: DataTable
        @param dataTable: Contains the data to plot.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotRange: PlotRange
        @param plotRange: The bounding box of plot coordinates that this function will expand.
        """

        self._saveContext(dataTable)

        self.checkRoles(["x", "y", "x-errorbar", "x-errorbar-up", "x-errorbar-down", "y-errorbar", "y-errorbar-up", "y-errorbar-down", "weight"])

        xExpression = self.xpath("pmml:PlotNumericExpression[@role='x']")
        yExpression = self.xpath("pmml:PlotNumericExpression[@role='y']")

        cutExpression = self.xpath("pmml:PlotSelection")

        exExpression = self.xpath("pmml:PlotNumericExpression[@role='x-errorbar']")
        exupExpression = self.xpath("pmml:PlotNumericExpression[@role='x-errorbar-up']")
        exdownExpression = self.xpath("pmml:PlotNumericExpression[@role='x-errorbar-down']")

        eyExpression = self.xpath("pmml:PlotNumericExpression[@role='y-errorbar']")
        eyupExpression = self.xpath("pmml:PlotNumericExpression[@role='y-errorbar-up']")
        eydownExpression = self.xpath("pmml:PlotNumericExpression[@role='y-errorbar-down']")

        weightExpression = self.xpath("pmml:PlotNumericExpression[@role='weight']")

        if len(xExpression) != 1 or len(yExpression) != 1:
            raise defs.PmmlValidationError("PlotScatter requires two PlotNumericExpressions, one with role \"x\", the other with role \"y\"")

        xValues = xExpression[0].evaluate(dataTable, functionTable, performanceTable)
        yValues = yExpression[0].evaluate(dataTable, functionTable, performanceTable)

        if len(cutExpression) == 1:
            selection = cutExpression[0].select(dataTable, functionTable, performanceTable)
        else:
            selection = NP("ones", len(dataTable), NP.dtype(bool))

        if len(exExpression) == 0 and len(exupExpression) == 0 and len(exdownExpression) == 0:
            exup, exdown = None, None
        elif len(exExpression) == 1 and len(exupExpression) == 0 and len(exdownExpression) == 0:
            exup = exExpression[0].evaluate(dataTable, functionTable, performanceTable)
            exdown = None
        elif len(exExpression) == 0 and len(exupExpression) == 1 and len(exdownExpression) == 1:
            exup = exupExpression[0].evaluate(dataTable, functionTable, performanceTable)
            exdown = exdownExpression[0].evaluate(dataTable, functionTable, performanceTable)
        else:
            raise defs.PmmlValidationError("Use \"x-errorbar\" for symmetric error bars or \"x-errorbar-up\" and \"x-errorbar-down\" for asymmetric errorbars, but no other combinations")

        if len(eyExpression) == 0 and len(eyupExpression) == 0 and len(eydownExpression) == 0:
            eyup, eydown = None, None
        elif len(eyExpression) == 1 and len(eyupExpression) == 0 and len(eydownExpression) == 0:
            eyup = eyExpression[0].evaluate(dataTable, functionTable, performanceTable)
            eydown = None
        elif len(eyExpression) == 0 and len(eyupExpression) == 1 and len(eydownExpression) == 1:
            eyup = eyupExpression[0].evaluate(dataTable, functionTable, performanceTable)
            eydown = eydownExpression[0].evaluate(dataTable, functionTable, performanceTable)
        else:
            raise defs.PmmlValidationError("Use \"y-errorbar\" for symmetric error bars or \"y-errorbar-up\" and \"y-errorbar-down\" for asymmetric errorbars, but no other combinations")

        if len(weightExpression) == 1:
            weight = weightExpression[0].evaluate(dataTable, functionTable, performanceTable)
        else:
            weight = None

        performanceTable.begin("PlotScatter prepare")

        if xValues.mask is not None:
            NP("logical_and", selection, NP(xValues.mask == defs.VALID), selection)
        if yValues.mask is not None:
            NP("logical_and", selection, NP(yValues.mask == defs.VALID), selection)

        if exup is not None and exup.mask is not None:
            NP("logical_and", selection, NP(exup.mask == defs.VALID), selection)
        if exdown is not None and exdown.mask is not None:
            NP("logical_and", selection, NP(exdown.mask == defs.VALID), selection)
        if eyup is not None and eyup.mask is not None:
            NP("logical_and", selection, NP(eyup.mask == defs.VALID), selection)
        if eydown is not None and eydown.mask is not None:
            NP("logical_and", selection, NP(eydown.mask == defs.VALID), selection)

        state.x = xValues.data[selection]
        state.y = yValues.data[selection]

        state.exup, state.exdown, state.eyup, state.eydown = None, None, None, None
        if exup is not None:
            state.exup = exup.data[selection]
        if exdown is not None:
            state.exdown = exdown.data[selection]
        if eyup is not None:
            state.eyup = eyup.data[selection]
        if eydown is not None:
            state.eydown = eydown.data[selection]

        state.weight = None
        if weight is not None:
            state.weight = weight.data[selection]

        stateId = self.get("stateId")
        if stateId is not None:
            persistentState = dataTable.state.get(stateId)
            if persistentState is None:
                persistentState = {}
                dataTable.state[stateId] = persistentState
            else:
                state.x = NP("concatenate", (persistentState["x"], state.x))
                state.y = NP("concatenate", (persistentState["y"], state.y))

                if exup is not None:
                    state.exup = NP("concatenate", (persistentState["exup"], state.exup))
                if exdown is not None:
                    state.exdown = NP("concatenate", (persistentState["exdown"], state.exdown))
                if eyup is not None:
                    state.eyup = NP("concatenate", (persistentState["eyup"], state.eyup))
                if eydown is not None:
                    state.eydown = NP("concatenate", (persistentState["eydown"], state.eydown))

                if weight is not None:
                    state.weight = NP("concatenate", (persistentState["weight"], state.weight))

            persistentState["x"] = state.x
            persistentState["y"] = state.y

            if exup is not None:
                persistentState["exup"] = state.exup
            if exdown is not None:
                persistentState["exdown"] = state.exdown
            if eyup is not None:
                persistentState["eyup"] = state.eyup
            if eydown is not None:
                persistentState["eydown"] = state.eydown

            if weight is not None:
                persistentState["weight"] = state.weight

        plotRange.expand(state.x, state.y, xValues.fieldType, yValues.fieldType)
        performanceTable.end("PlotScatter prepare")

    def draw(self, state, plotCoordinates, plotDefinitions, performanceTable):
        """Draw the plot element.

        This stage consists of creating an SVG image of the
        pre-computed data.

        @type state: ad-hoc Python object
        @param state: State information that persists long enough to use quantities computed in C{prepare} in the C{draw} stage.  This is a work-around of lxml's refusal to let its Python instances maintain C{self} and it is unrelated to DataTableState.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot element will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot element.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotScatter draw")

        output = svg.g()

        marker = self._makeMarker(plotDefinitions)
        plotDefinitions[marker.get("id")] = marker

        # selection = NP("isfinite", state.x)
        # NP("logical_and", selection, NP("isfinite", state.y))
        # state.x = state.x[selection]
        # state.y = state.y[selection]

        plotx, ploty, plotexup, plotexdown, ploteyup, ploteydown, plotweight = None, None, None, None, None, None, None

        if self.get("limit") is not None and int(self.get("limit")) < len(state.x):
            indexes = random.sample(xrange(len(state.x)), int(self.get("limit")))
            plotx = state.x[indexes]
            ploty = state.y[indexes]

            if state.exup is not None:
                if state.exdown is not None:
                    plotexup = NP(plotx + NP("absolute", state.exup[indexes]))
                    plotexdown = NP(plotx - NP("absolute", state.exdown[indexes]))
                else:
                    plotexup = NP(plotx + NP("absolute", state.exup[indexes]))
                    plotexdown = NP(plotx - NP("absolute", state.exup[indexes]))

            if state.eyup is not None:
                if state.eydown is not None:
                    ploteyup = NP(ploty + NP("absolute", state.eyup[indexes]))
                    ploteydown = NP(ploty - NP("absolute", state.eydown[indexes]))
                else:
                    ploteyup = NP(ploty + NP("absolute", state.eyup[indexes]))
                    ploteydown = NP(ploty - NP("absolute", state.eyup[indexes]))

            if state.weight is not None:
                plotweight = state.weight[indexes]

        else:
            plotx = state.x
            ploty = state.y

            if state.exup is not None:
                if state.exdown is not None:
                    plotexup = NP(plotx + NP("absolute", state.exup))
                    plotexdown = NP(plotx - NP("absolute", state.exdown))
                else:
                    plotexup = NP(plotx + NP("absolute", state.exup))
                    plotexdown = NP(plotx - NP("absolute", state.exup))

            if state.eyup is not None:
                if state.eydown is not None:
                    ploteyup = NP(ploty + NP("absolute", state.eyup))
                    ploteydown = NP(ploty - NP("absolute", state.eydown))
                else:
                    ploteyup = NP(ploty + NP("absolute", state.eyup))
                    ploteydown = NP(ploty - NP("absolute", state.eyup))

            if state.weight is not None:
                plotweight = state.weight

        if plotexup is not None:
            plotexup, dummy = plotCoordinates(plotexup, ploty)
        if plotexdown is not None:
            plotexdown, dummy = plotCoordinates(plotexdown, ploty)
        if ploteyup is not None:
            dummy, ploteyup = plotCoordinates(plotx, ploteyup)
        if ploteydown is not None:
            dummy, ploteydown = plotCoordinates(plotx, ploteydown)

        plotx, ploty = plotCoordinates(plotx, ploty)

        style = self.getStyleState()
        strokeStyle = dict((x, style[x]) for x in style if x.startswith("stroke"))
        output.extend(self.drawErrorbars(plotx, ploty, plotexup, plotexdown, ploteyup, ploteydown, float(style["marker-size"]), strokeStyle, weight=plotweight))

        markerReference = "#" + marker.get("id")
        if plotweight is None:
            output.extend(svg.use(**{"x": repr(x), "y": repr(y), defs.XLINK_HREF: markerReference}) for x, y in itertools.izip(plotx, ploty))
        else:
            output.extend(svg.use(**{"x": repr(x), "y": repr(y), "style": "opacity: %r;" % w, defs.XLINK_HREF: markerReference}) for x, y, w in itertools.izip(plotx, ploty, plotweight))

        svgId = self.get("svgId")
        if svgId is not None:
            output["id"] = svgId

        performanceTable.end("PlotScatter draw")
        return output
Exemple #36
0
 def style(self, value):
     self.set("style", PlotStyle.toString(value))
Exemple #37
0
    def draw(self, dataTable, functionTable, performanceTable, plotCoordinates, plotContentBox, plotDefinitions):
        """Draw the plot annotation.

        @type dataTable: DataTable
        @param dataTable: Contains the data to plot, if any.
        @type functionTable: FunctionTable
        @param functionTable: Defines functions that may be used to transform data for plotting.
        @type performanceTable: PerformanceTable
        @param performanceTable: Measures and records performance (time and memory consumption) of the drawing process.
        @type plotCoordinates: PlotCoordinates
        @param plotCoordinates: The coordinate system in which this plot will be placed.
        @type plotContentBox: PlotContentBox
        @param plotContentBox: A bounding box in which this plot will be placed.
        @type plotDefinitions: PlotDefinitions
        @type plotDefinitions: The dictionary of key-value pairs that forms the <defs> section of the SVG document.
        @rtype: SvgBinding
        @return: An SVG fragment representing the fully drawn plot.
        """

        svg = SvgBinding.elementMaker
        performanceTable.begin("PlotLegend")

        # figure out how to format text
        style = self.getStyleState()
        textStyle = {"fill": style["text-color"], "stroke": "none"}
        for styleProperty in "font", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight":
            if styleProperty in style:
                textStyle[styleProperty] = style[styleProperty]
        labelAttributes = {"font-size": style["font-size"], defs.XML_SPACE: "preserve", "style": PlotStyle.toString(textStyle)}

        columnAlign = style["column-align"]
        if not set(columnAlign.lower()).issubset(set(["l", "m", "r", "."])):
            raise defs.PmmlValidationError("PlotLegend's column-align style property may only contain the following characters: \"l\", \"m\", \"r\", \".\"")

        columnPadding = float(style["column-padding"])

        ### get an <svg:text> object for each cell

        # content follows the same delimiter logic as Array, except that lineseps outside of quotes signify new table rows
        rowIndex = 0
        colIndex = 0
        cellContents = {}
        for item in sum([[x, x.tail] for x in self.childrenOfClass(PmmlPlotLegendContent)], [self.text]):
            if item is None: pass

            elif isinstance(item, basestring):
                for word in re.finditer(self._re_word, item):
                    one, two, three = word.groups()

                    # quoted text; take it all as-is, without the outermost quotes and unquoting quoted quotes
                    if two is not None:
                        cellContents[rowIndex, colIndex] = svg.text(two.replace(r'\"', '"'), **labelAttributes)
                        colIndex += 1

                    elif one == r'""':
                        colIndex += 1

                    else:
                        newlineIndex = one.find(os.linesep)
                        if newlineIndex == 0 and not (rowIndex == 0 and colIndex == 0):
                            rowIndex += 1
                            colIndex = 0
                        while newlineIndex != -1:
                            if one[:newlineIndex] != "":
                                cellContents[rowIndex, colIndex] = svg.text(one[:newlineIndex], **labelAttributes)
                                rowIndex += 1
                                colIndex = 0

                            one = one[(newlineIndex + len(os.linesep)):]
                            newlineIndex = one.find(os.linesep)

                        if one != "":
                            cellContents[rowIndex, colIndex] = svg.text(one, **labelAttributes)
                            colIndex += 1

            else:
                performanceTable.pause("PlotLegend")
                rowIndex, colIndex = item.draw(dataTable, functionTable, performanceTable, rowIndex, colIndex, cellContents, labelAttributes, plotDefinitions)
                performanceTable.unpause("PlotLegend")

        maxRows = 0
        maxCols = 0
        maxChars = {}
        beforeDot = {}
        afterDot = {}
        for row, col in cellContents:
            if row > maxRows:
                maxRows = row
            if col > maxCols:
                maxCols = col

            if col >= len(columnAlign):
                alignment = columnAlign[-1]
            else:
                alignment = columnAlign[col]

            if col not in maxChars:
                maxChars[col] = 0
                beforeDot[col] = 0
                afterDot[col] = 0

            textContent = cellContents[row, col].text
            if textContent is not None:
                if len(textContent) > maxChars[col]:
                    maxChars[col] = len(textContent)

                if alignment == ".":
                    dotPosition = textContent.find(".")
                    if dotPosition == -1:
                        dotPosition = textContent.find("e")
                        if dotPosition == -1:
                            dotPosition = textContent.find("E")
                            if dotPosition == -1:
                                dotPosition = textContent.find(u"\u00d710")
                                if dotPosition == -1:
                                    dotPosition = len(textContent)
                    if dotPosition > beforeDot[col]:
                        beforeDot[col] = dotPosition
                    if len(textContent) - dotPosition > afterDot[col]:
                        afterDot[col] = len(textContent) - dotPosition
        
        maxRows += 1
        maxCols += 1
        for col in xrange(maxCols):
            if beforeDot[col] + afterDot[col] > maxChars[col]:
                maxChars[col] = beforeDot[col] + afterDot[col]
        cellWidthDenom = float(sum(maxChars.values()))

        ### create a subContentBox and fill the table cells

        svgId = self.get("svgId")
        content = []
        if svgId is None: attrib = {}
        else: attrib = {"id": svgId}

        # change some of the margins based on text, unless overridden by explicit styleProperties

        if style.get("margin-bottom") == "auto": del style["margin-bottom"]
        if style.get("margin-top") == "auto": del style["margin-top"]
        if style.get("margin-left") == "auto": del style["margin-left"]
        if style.get("margin-right") == "auto": del style["margin-right"]

        subContentBox = plotContentBox.subContent(style)
        nominalHeight = maxRows * float(style["font-size"])
        nominalWidth = cellWidthDenom * 0.5*float(style["font-size"]) + columnPadding * (maxCols - 1)

        if nominalHeight < subContentBox.height:
            if "margin-bottom" in style and "margin-top" in style:
                pass
            elif "margin-bottom" in style:
                style["margin-top"] = subContentBox.height - nominalHeight
            elif "margin-top" in style:
                style["margin-bottom"] = subContentBox.height - nominalHeight
            else:
                style["margin-bottom"] = style["margin-top"] = (subContentBox.height - nominalHeight) / 2.0

        if nominalWidth < subContentBox.width:
            if "margin-left" in style and "margin-right" in style:
                pass
            elif "margin-left" in style:
                style["margin-right"] = subContentBox.width - nominalWidth
            elif "margin-right" in style:
                style["margin-left"] = subContentBox.width - nominalWidth
            else:
                style["margin-left"] = style["margin-right"] = (subContentBox.width - nominalWidth) / 2.0

        subContentBox = plotContentBox.subContent(style)
        borderRect = plotContentBox.border(style)

        ### create a border rectangle
        if borderRect is not None:
            rectStyle = {"fill": style["background"], "stroke": "none"}
            if "background-opacity" in style:
                rectStyle["fill-opacity"] = style["background-opacity"]

            x1 = borderRect.x
            y1 = borderRect.y
            x2 = borderRect.x + borderRect.width
            y2 = borderRect.y + borderRect.height
            x1, y1 = plotCoordinates(x1, y1)
            x2, y2 = plotCoordinates(x2, y2)

            subAttrib = {"x": repr(x1), "y": repr(y1), "width": repr(x2 - x1), "height": repr(y2 - y1), "style": PlotStyle.toString(rectStyle)}
            if svgId is not None:
                subAttrib["id"] = svgId + ".background"

            if rectStyle["fill"] != "none":
                content.append(svg.rect(**subAttrib))

        ### put the cell content in the table
        if subContentBox is not None:
            cellHeight = subContentBox.height / float(maxRows)
            colStart = [subContentBox.x]
            for col in xrange(maxCols):
                colStart.append(colStart[col] + subContentBox.width * maxChars[col] / cellWidthDenom)

            for row in xrange(maxRows):
                for col in xrange(maxCols):
                    cellContent = cellContents.get((row, col))
                    if cellContent is not None:
                        if col >= len(columnAlign):
                            alignment = columnAlign[-1]
                        else:
                            alignment = columnAlign[col]

                        textContent = None
                        if cellContent.tag == "text" or cellContent.tag[-5:] == "}text":
                            if alignment.lower() == "l":
                                cellContent.set("text-anchor", "start")
                            elif alignment.lower() == "m":
                                cellContent.set("text-anchor", "middle")
                            elif alignment.lower() == "r":
                                cellContent.set("text-anchor", "end")
                            elif alignment.lower() == ".":
                                cellContent.set("text-anchor", "middle")
                            textContent = cellContent.text

                        if alignment.lower() == ".":
                            if textContent is None:
                                alignment = "m"
                            else:
                                dotPosition = textContent.find(".")
                                if dotPosition == -1:
                                    dotPosition = textContent.find("e")
                                    if dotPosition == -1:
                                        dotPosition = textContent.find("E")
                                        if dotPosition == -1:
                                            dotPosition = textContent.find(u"\u00d710")
                                            if dotPosition == -1:
                                                dotPosition = len(textContent) - 0.3
                                dotPosition += 0.2*textContent[:int(math.ceil(dotPosition))].count(u"\u2212")

                                x = (colStart[col] + colStart[col + 1]) / 2.0
                                x -= (dotPosition - 0.5*len(textContent) + 0.5) * nominalWidth/cellWidthDenom

                        if alignment.lower() == "l":
                            x = colStart[col]
                        elif alignment.lower() == "m":
                            x = (colStart[col] + colStart[col + 1]) / 2.0
                        elif alignment.lower() == "r":
                            x = colStart[col + 1]

                        y = subContentBox.y + cellHeight * (row + 0.75)
                        x, y = plotCoordinates(x, y)

                        cellContent.set("transform", "translate(%r,%r)" % (x, y))
                        content.append(cellContent)
        
        ### create a border rectangle (reuses subAttrib, replaces subAttrib["style"])
        if borderRect is not None:
            rectStyle = {"stroke": style["border-color"]}
            if rectStyle["stroke"] != "none":
                for styleProperty in "border-dasharray", "border-dashoffset", "border-linecap", "border-linejoin", "border-miterlimit", "border-opacity", "border-width":
                    if styleProperty in style:
                        rectStyle[styleProperty.replace("border-", "stroke-")] = style[styleProperty]

                subAttrib["style"] = PlotStyle.toString(rectStyle)
                if svgId is not None:
                    subAttrib["id"] = svgId + ".border"
                content.append(svg.rect(**subAttrib))

        performanceTable.end("PlotLegend")
        return svg.g(*content, **attrib)