Skip to content

Commit

Permalink
Add ListStepPlot refactor _ListPlot a little (#1096)
Browse files Browse the repository at this point in the history
* Add `ListStepPlot[]` 
* Refactor `_ListPlot[]` a little; note this is an abstract class. Add a `ListPlotType` enum
* `mathics.eval.plot` -> `mathics.eval.drawing.plot`
  • Loading branch information
rocky authored Sep 27, 2024
1 parent f835a00 commit 8e0aff8
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 47 deletions.
2 changes: 1 addition & 1 deletion mathics/builtin/atomic/symbols.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ class Names(Builtin):
The wildcard '*' matches any character:
>> Names["List*"]
= {List, ListLinePlot, ListLogPlot, ListPlot, ListQ, Listable}
= {List, ListLinePlot, ListLogPlot, ListPlot, ListQ, ListStepPlot, Listable}
The wildcard '@' matches only lowercase characters:
>> Names["List@"]
Expand Down
80 changes: 71 additions & 9 deletions mathics/builtin/drawing/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import itertools
import numbers
from abc import ABC
from functools import lru_cache
from math import cos, pi, sin, sqrt
from typing import Callable, Optional
Expand Down Expand Up @@ -44,13 +45,14 @@
SymbolSlot,
SymbolStyle,
)
from mathics.eval.nevaluator import eval_N
from mathics.eval.plot import (
from mathics.eval.drawing.plot import (
ListPlotType,
compile_quiet_function,
eval_ListPlot,
eval_Plot,
get_plot_range,
)
from mathics.eval.nevaluator import eval_N

# This tells documentation how to sort this module
# Here we are also hiding "drawing" since this erroneously appears at the top level.
Expand Down Expand Up @@ -274,7 +276,7 @@ def color_data_function(self, name):
return Expression(SymbolColorDataFunction, *arguments)


class _ListPlot(Builtin):
class _ListPlot(Builtin, ABC):
"""
Base class for ListPlot, and ListLinePlot
2-Dimensional plot a list of points in some fashion.
Expand All @@ -295,7 +297,7 @@ class _ListPlot(Builtin):
def eval(self, points, evaluation: Evaluation, options: dict):
"%(name)s[points_, OptionsPattern[%(name)s]]"

plot_name = self.get_name()
class_name = self.__class__.__name__

# Scale point values down by Log 10. Tick mark values will be adjusted to be 10^n in GraphicsBox.
if self.use_log_scale:
Expand All @@ -310,6 +312,15 @@ def eval(self, points, evaluation: Evaluation, options: dict):
# FIXME: arrange for self to have a .symbolname property or attribute
expr = Expression(Symbol(self.get_name()), points, *options_to_rules(options))

if class_name == "ListPlot":
plot_type = ListPlotType.ListPlot
elif class_name == "ListLinePlot":
plot_type = ListPlotType.ListLinePlot
elif class_name == "ListStepPlot":
plot_type = ListPlotType.ListStepPlot
else:
plot_type = None

plotrange_option = self.get_option(options, "PlotRange", evaluation)
plotrange = eval_N(plotrange_option, evaluation).to_python()
if plotrange == "System`All":
Expand Down Expand Up @@ -355,7 +366,7 @@ def eval(self, points, evaluation: Evaluation, options: dict):
joined_option = self.get_option(options, "Joined", evaluation)
is_joined_plot = joined_option.to_python()
if is_joined_plot not in [True, False]:
evaluation.message(plot_name, "joind", joined_option, expr)
evaluation.message(class_name, "joind", joined_option, expr)
is_joined_plot = False

return eval_ListPlot(
Expand All @@ -366,6 +377,7 @@ def eval(self, points, evaluation: Evaluation, options: dict):
is_joined_plot=is_joined_plot,
filling=filling,
use_log_scale=self.use_log_scale,
list_plot_type=plot_type,
options=options,
)

Expand All @@ -382,7 +394,7 @@ def colors(self):
return colors


class _Plot(Builtin):
class _Plot(Builtin, ABC):
attributes = A_HOLD_ALL | A_PROTECTED | A_READ_PROTECTED

expect_list = False
Expand Down Expand Up @@ -1633,7 +1645,7 @@ def eval(
# This includes the plot data, and overall graphics directives
# like the Hue.

for index, f in enumerate(functions):
for f in functions:
# list of all plotted points for a given function
plot_points = []

Expand All @@ -1653,8 +1665,8 @@ def apply_fn(fn: Callable, x_value: int) -> Optional[float]:
plot_points.append((x_value, point))

x_range = get_plot_range(
[xx for xx, yy in base_plot_points],
[xx for xx, yy in plot_points],
[xx for xx, _ in base_plot_points],
[xx for xx, _ in plot_points],
x_range,
)
plot_groups.append(plot_points)
Expand All @@ -1680,6 +1692,7 @@ def apply_fn(fn: Callable, x_value: int) -> Optional[float]:
is_joined_plot=False,
filling=False,
use_log_scale=False,
list_plot_type=ListPlotType.DiscretePlot,
options=options,
)

Expand Down Expand Up @@ -2035,6 +2048,55 @@ class ListLinePlot(_ListPlot):
summary_text = "plot lines through lists of points"


class ListStepPlot(_ListPlot):
"""
<url>
:WMA link:
https://reference.wolfram.com/language/ref/ListStepPlot.html</url>
<dl>
<dt>'ListStepPlot[{$y_1$, $y_2$, ...}]'
<dd>plots a line through a list of $y$-values, assuming integer $x$-values 1, 2, 3, ...
<dt>'ListStepPlot[{{$x_1$, $y_1$}, {$x_2$, $y_2$}, ...}]'
<dd>plots a line through a list of $x$, $y$ pairs.
<dt>'ListStepPlot[{$list_1$, $list_2$, ...}]'
<dd>plots several lines.
</dl>
>> ListStepPlot[{1, 1, 2, 3, 5, 8, 13, 21}]
= -Graphics-
'ListStepPlot' accepts a superset of the Graphics options. \
By default, 'ListStepPlot's are joined, but that can be disabled.
>> ListStepPlot[{1, 1, 2, 3, 5, 8, 13, 21}, Joined->False]
= -Graphics-
The same as the first example but using a list of point as data, \
and filling the plot to the x axis.
>> ListStepPlot[{{1, 1}, {3, 2}, {4, 5}, {5, 8}, {6, 13}, {7, 21}}, Filling->Axis]
= -Graphics-
"""

attributes = A_HOLD_ALL | A_PROTECTED

options = Graphics.options.copy()
options.update(
{
"Axes": "True",
"AspectRatio": "1 / GoldenRatio",
"Mesh": "None",
"PlotRange": "Automatic",
"PlotPoints": "None",
"Filling": "None",
"Joined": "True",
}
)
summary_text = "plot values in steps"


class ListLogPlot(_ListPlot):
"""
<url>:WMA link: https://reference.wolfram.com/language/ref/ListLogPlot.html</url>
Expand Down
Empty file.
126 changes: 89 additions & 37 deletions mathics/eval/plot.py → mathics/eval/drawing/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
That is done as another pass after M-expression evaluation finishes.
"""

from enum import Enum
from math import cos, isinf, isnan, pi, sqrt
from typing import Callable, Iterable, List, Optional, Type, Union

Expand All @@ -30,6 +31,14 @@
SymbolPolygon,
)

ListPlotNames = (
"DiscretePlot",
"ListPlot",
"ListLinePlot",
"ListStepPlot",
)
ListPlotType = Enum("ListPlotType", ListPlotNames)

RealPoint6 = Real(0.6)
RealPoint2 = Real(0.2)

Expand Down Expand Up @@ -129,17 +138,21 @@ def eval_ListPlot(
is_joined_plot: bool,
filling,
use_log_scale: bool,
list_plot_type: ListPlotType,
options: dict,
):
"""
Evaluation part of LisPlot[] and DiscretePlot[]
plot_groups: the plot point data, It can be in a number of different list formats
x_range: the x range that of the area to show in the plot
y_range: the y range that of the area to show in the plot
is_discrete_plot: True if called from DiscretePlot, False if called from ListPlot
is_joined_plot: True if points are to be joined. This never happens in a discrete plot
options: miscellaneous graphics options from underlying M-Expression
Evaluation part of ListPlot like plots. eg DiscretePlot[], ListPlot[], ListLinePlot[], etc.
which are enumerated in ListPlotType.
Parameters;
plot_groups: the plot point data, It can be in a number of different list formats
x_range: the x range that of the area to show in the plot
y_range: the y range that of the area to show in the plot
is_discrete_plot: True if called from DiscretePlot, False if called from ListPlot
is_joined_plot: True if points are to be joined. This never happens in a discrete plot
list_plot_type: the kinds of ListPlots we handle
options: miscellaneous graphics options from underlying M-Expression
"""

if not isinstance(plot_groups, list) or len(plot_groups) == 0:
Expand Down Expand Up @@ -219,6 +232,9 @@ def eval_ListPlot(
i = 0
while i < len(plot_groups[lidx]):
seg = plot_group[i]
# skip empty segments How do they get in though?
if not seg:
continue
for j, point in enumerate(seg):
x_min = min(x_min, point[0])
x_max = max(x_min, point[0])
Expand All @@ -232,6 +248,28 @@ def eval_ListPlot(
plot_groups[lidx][i + 1] = seg[j + 1 :]
i -= 1
break
pass

# For step plots, we have 2n points; n -1 of these
# we create from the n points by
# insert a new point from the y coordinate
# of the previous point in between each new point
# other than the first point. The last plot point
# has the preview plot point y value and the average
# step value added to the last x value
if list_plot_type == ListPlotType.ListStepPlot:
step_plot_group = []
last_point = seg[0]
for j, point in enumerate(seg):
if j != 0:
step_plot_group.append([point[0], last_point[1]])
step_plot_group.append(point)
step_plot_group.append(point)
last_point = point
last_x = last_point[0]
average = last_x + ((seg[0][0] + last_x) / 2)
step_plot_group.append((average, last_point[1]))
plot_groups[lidx][i] = step_plot_group

i += 1

Expand Down Expand Up @@ -269,43 +307,57 @@ def eval_ListPlot(
for index, plot_group in enumerate(plot_groups):
graphics.append(Expression(SymbolHue, Real(hue), RealPoint6, RealPoint6))
for segment in plot_group:
mathics_segment = from_python(segment)
if is_joined_plot:
graphics.append(Expression(SymbolLine, mathics_segment))
if filling is not None:
graphics.append(
Expression(
SymbolHue, Real(hue), RealPoint6, RealPoint6, RealPoint2
if not is_joined_plot and list_plot_type == ListPlotType.ListStepPlot:
line_segments = [
(segment[i], segment[i + 1])
for i in range(0, len(segment) - 1)
if segment[i][0] != segment[i + 1][0]
]
for line_segment in line_segments:
graphics.append(Expression(SymbolLine, from_python(line_segment)))
pass
else:
mathics_segment = from_python(segment)
if is_joined_plot:
graphics.append(Expression(SymbolLine, mathics_segment))
if filling is not None:
graphics.append(
Expression(
SymbolHue, Real(hue), RealPoint6, RealPoint6, RealPoint2
)
)
)
fill_area = list(segment)
fill_area.append([segment[-1][0], filling])
fill_area.append([segment[0][0], filling])
graphics.append(Expression(SymbolPolygon, from_python(fill_area)))
elif is_axis_filling:
graphics.append(Expression(SymbolPoint, mathics_segment))
for mathics_point in mathics_segment:
graphics.append(
Expression(
SymbolLine,
ListExpression(
ListExpression(mathics_point[0], Integer0),
mathics_point,
),
fill_area = list(segment)
fill_area.append([segment[-1][0], filling])
fill_area.append([segment[0][0], filling])
graphics.append(
Expression(SymbolPolygon, from_python(fill_area))
)
)
else:
graphics.append(Expression(SymbolPoint, mathics_segment))
if filling is not None:
for point in segment:
elif is_axis_filling:
graphics.append(Expression(SymbolPoint, mathics_segment))
for mathics_point in mathics_segment:
graphics.append(
Expression(
SymbolLine,
from_python(
[[point[0], filling], [point[0], point[1]]]
ListExpression(
ListExpression(mathics_point[0], Integer0),
mathics_point,
),
)
)
else:
graphics.append(Expression(SymbolPoint, mathics_segment))
if filling is not None:
for point in segment:
graphics.append(
Expression(
SymbolLine,
from_python(
[[point[0], filling], [point[0], point[1]]]
),
)
)
pass
pass

if index % 4 == 0:
hue += hue_pos
Expand Down

0 comments on commit 8e0aff8

Please sign in to comment.