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 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 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 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)
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
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)
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
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)
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
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
def style(self): s = self.get("style") if s is None: s = self.get("style", defaultFromXsd=True) self.set("style", s) return PlotStyle(self)
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)
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
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)
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
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
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)
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)
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)
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)
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
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
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)
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
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, 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 style(self, value): self.set("style", PlotStyle.toString(value))
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
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)