Visualización
scene.viz: espacios de coordenadas, funciones, cálculo, datos, valores reactivos, gráficos declarativos y campos vectoriales
scene.viz reúne lo que convierte números en imágenes: espacios de coordenadas tipados, funciones y construcciones de cálculo, series de datos, valores reactivos, gráficos declarativos y campos vectoriales. Todo trabaja en coordenadas de datos: al mover o escalar el espacio, curvas, puntos y etiquetas lo acompañan. Las matrices están en Matrices.
import math
from gaanim import Axis, BLUE, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
plane = scene.viz.cartesian_2d(Axis.linear(-6, 6).ticks(1), Axis.linear(-3, 3).ticks(1))
amplitude = scene.viz.parameter(1.0)
wave = plane.plot(lambda x, a: a * math.sin(x), inputs=[amplitude]).stroke(BLUE, 0.05)
scene.play([plane.animate.create(), wave.animate.create()], duration=1.0)
scene.play([amplitude.animate.set(2.5).duration(1.0)])
scene.render()
Elige la herramienta según los datos:
- Un espacio tipado (
cartesian_2d,polar,complex,number_line) cuando las coordenadas tienen significado matemático continuo: funciones, cálculo, campos y geometría propia. ChartSpeccuando cada fila de una tabla es una observación: marcas, codificaciones, escalas y transiciones con identidad estable.
Espacios cartesianos
Un plano con ejes, rejilla, marcas y números que conoce su escala. Todas las curvas y marcas que crea viven en sus coordenadas de datos.
Cartesian2D (un CoordinateSpace). numbers controla el texto de las marcas y labels, los títulos de Axis.label. Una capa desactivada sigue disponible como un Drawable vacío, así que el conjunto de capas es estable para componer y animar. Los números negativos usan el signo menos tipográfico (−2). Los colores salen del tema (axes/axis, axes/grid, axes/numbers…).| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
x / y | Axis | obligatorio | Ejes con dominio, marcas y títulos (ver «Ejes»). |
width / height | float | None | None | Tamaño en unidades de escena; por defecto, relativo al área segura. |
grid / axes / ticks / numbers / labels | bool | True | Interruptores globales de cada capa. |
x_axis / y_axis, x_grid / y_grid, x_ticks / y_ticks, x_numbers / y_numbers, x_labels / y_labels | bool | None | None | Sustituyen el interruptor global en un eje; None lo hereda. El global es axes y el de cada eje x_axis y y_axis, en singular. |
plane = scene.viz.cartesian_2d(
Axis.linear(-4, 4).ticks(1).label("x"),
Axis.linear(-2, 2).ticks(1).label("y"),
grid=False, x_grid=True,
numbers=False, y_numbers=True,
)
scene.play([plane.animate.write().duration(1.2)])y = f(x). Con inputs, la curva se regenera cuando cambia el valor resuelto por la línea de tiempo, también al hacer seek. Se muestrea con más detalle si el plano tiene vistas ampliadas con view_to.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
function | Callable[..., float] | obligatorio | Recibe la coordenada x y después los valores de inputs. |
domain | (float, float) | None | None | Intervalo de x; sin él cubre la unión de todas las ventanas de view_to del plano. |
samples / tolerance | int | None / float | None / 0.0075 | Muestras fijas, o muestreo adaptativo con esa tolerancia. |
derivative | Callable | None | None | Derivada explícita con la misma firma; no hay diferenciación implícita. |
inputs | Sequence[Parameter | Variable | Computed | TimeInput] | () | Dependencias reactivas en orden. |
plane = scene.viz.cartesian_2d(Axis.linear(-4, 4), Axis.linear(-2, 2))
curve = plane.plot(math.sin, domain=(-math.pi, math.pi)).stroke(BLUE, 0.04)t → (x, y) sobre domain, con el mismo muestreo y las mismas inputs que plot.plane = scene.viz.cartesian_2d(Axis.linear(-3, 3), Axis.linear(-2, 2))
lissajous = plane.parametric(lambda t: (2 * math.sin(3 * t), 1.5 * math.sin(2 * t)), (0, math.tau))f(x, y), muestreada en una rejilla de resolution celdas.plane = scene.viz.cartesian_2d(Axis.linear(-3, 3), Axis.linear(-2, 2))
ellipse = plane.implicit(lambda x, y: x**2 / 4 + y**2 - 1)f(x, y) para cada valor de levels.plane = scene.viz.cartesian_2d(Axis.linear(-3, 3), Axis.linear(-2, 2))
rings = plane.contour(lambda x, y: x * x + y * y, levels=[0.5, 1, 2, 3])Drawable para darle estilo o animarla por separado: grid, major_grid, minor_grid, axis, axes, ticks, numbers o labels.plane = scene.viz.cartesian_2d(Axis.linear(-4, 4).ticks(1), Axis.linear(-2, 2).ticks(1))
plane.layer("grid").opacity(0.3)
scene.play([plane.layer("axes").animate.create(), plane.layer("numbers").animate.fade_in()])(x, y) del espacio. Pásala a Drawable.at_coordinate (o a CoordinateRef.place) para colocar un objeto como hijo del espacio, que seguirá sus cambios de vista.plane = scene.viz.cartesian_2d(Axis.linear(-4, 4), Axis.linear(-2, 2))
peak = scene.geometry.dot(0.1).fill(GOLD).at_coordinate(plane.coord(1, 1.5))
label = plane.coord(1, 1.8).place(scene.text("máximo", role="label"))PointRef de escena en el dato (x, y) para objetos que no son hijos del espacio (anotaciones, flechas, conectores). Se resuelve en cada fotograma con la ventana de view_to vigente y con la posición, escala y rotación del plano. Datos no finitos o fuera de escala lanzan ValueError.peak = plane.data_to_scene(2, 2)
note = scene.text("máximo").follow(peak, offset=(0.6, 0.4))
arrow = scene.geometry.connector(note, peak)
scene.play([plane.animate.view_to((0, 4), (0, 3)).duration(1.2)])plane.animate.view_to(...) la anima. El área de trazado no se mueve: ejes, rejillas, marcas y datos se recortan a ella, los títulos quedan fijos y los números y los marcadores de scatter_data conservan su tamaño. Los trazos mantienen su grosor. Los ejes con marcas automáticas regeneran marcas y rejilla con un fundido cruzado; los de paso fijo (.ticks(1)) lo conservan. Aplica los estilos de layer(...) antes del primer cambio de vista. Solo ejes lineales o temporales; otros lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
x_domain / y_domain | (float, float) | obligatorio | Nueva ventana de datos; finita y creciente. |
import math
from gaanim import Axis, GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
plane = scene.viz.cartesian_2d(Axis.linear(-6, 6), Axis.linear(-3, 3))
curve = plane.plot(lambda x: math.sin(2 * x)).stroke(GOLD, 0.05)
scene.play([plane.animate.create(), curve.animate.create()], duration=0.6)
scene.play([plane.animate.view_to((-1.5, 1.5), (-1.2, 1.2)).duration(1.2)])
scene.render()
Las marcas de datos de un espacio cartesiano (plot, parametric, scatter_data, campos…) se recortan siempre al área de trazado, haya o no view_to. mark.no_clip() deja que una marca concreta sobresalga. No se recortan los objetos colocados con at_coordinate ni las marcas de scene.viz.chart.
CoordinateSpaceAnimation: create() traza las capas con trazos independientes de la resolución, write() construye a la vez ejes, rejillas, marcas, números y títulos, y también ofrece fade_in, fade_out, move_to, scale_to, rotate_to y view_to. move_to, scale_to y rotate_to del propio espacio devuelven el espacio para encadenar, y drawable() da el objeto raíz para layout y estilo.Ejes
Axis describe dominio, escala, marcas, números, título, cruce y estilo de un eje. Es inmutable: cada método devuelve una copia.
linear para diferencias uniformes, log para órdenes de magnitud positivos, symlog para datos con signo alrededor de cero, power para potencias, time para marcas de tiempo en segundos y category para grupos discretos.frequency = Axis.log(0.1, 1000, base=10).ticks(10).label("frecuencia")
signed = Axis.symlog(-100, 100, threshold=1)
groups = Axis.category(["A", "B", "C"])step unidades de datos. Un paso fijo se conserva en cualquier vista.view_to.subdivisions partes con marcas menores.auto, fixed, scientific, percent, fraction, pi o datetime. precision se aplica a fixed, scientific y percent; denominator, a fraction y pi; pattern, a datetime. fraction y pi redondean al múltiplo de 1/denominator más cercano y simplifican la fracción: con denominator=2, pi rotula π/2, π, 3π/2 y 2π, y fraction rotula 1/2, 1 y 3/2.angle = Axis.linear(0, 2 * math.pi).ticks(math.pi / 2).numbers("pi", denominator=2)
share = Axis.linear(0, 1).ticks(0.25).numbers("percent", precision=0)position="end" (predeterminado) lo pone tras el extremo positivo y "start" tras el negativo; "center" (o "middle", "mid") centra x bajo sus números e y a la izquierda, girado 90°. En un eje vertical, "top" y "bottom" son alias de los extremos.y = Axis.linear(0, 1).label("valor relativo", position="center")"auto", "zero", "min", "max" o un valor de datos.color, width, tick_length, tick_width, tick_color, number_color, label_color); el resto se hereda del tema. Así, cambiar solo width conserva el color del tema.x = Axis.linear(-3, 3).ticks(1).style(color=WHITE, width=0.03)(mínimo, máximo) del eje.Construcciones de cálculo
Tangentes, secantes, áreas y sumas de Riemann sobre funciones del espacio.
function en x. length fija la longitud visible y dx el paso de la derivada numérica.from gaanim import Axis, BLUE, GOLD, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
plane = scene.viz.cartesian_2d(Axis.linear(-1, 5).ticks(1), Axis.linear(-1, 3).ticks(1))
f = lambda x: 0.15 * x * x + 0.3
curve = plane.plot(f).stroke(BLUE, 0.04)
area = plane.riemann_sum(f, (0, 4), rectangles=8).fill(GOLD).opacity(0.6)
touch = plane.tangent(f, 3, length=3).stroke(GOLD, 0.04)
scene.play([plane.animate.create(), curve.animate.create()], duration=0.6)
scene.play([area.animate.fade_in(), touch.animate.create()], duration=0.8)
scene.render()
function en x, con las mismas opciones que tangent.function en x0 y x1.chord = plane.secant(lambda x: 0.15 * x * x, 1, 4)function y baseline sobre domain.area = plane.area_under(math.sin, (0, math.pi)).fill(BLUE).opacity(0.5)rectangles de ancho igual y altura evaluada según method (left, midpoint, middle o right).(x, y) hasta los dos ejes.guides = plane.projections(2.5, 1.8)Series de datos
plot_data y scatter_data dibujan series (xs, ys) directamente en las coordenadas del espacio: si el plano se mueve o escala, la serie lo acompaña.
Parameter.drive_from_samples.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
xs, ys | Sequence[float | None] | obligatorio | Series de igual longitud; None es una muestra ausente. |
step | bool | False | Gráfica escalonada. |
baseline | float | None | None | Con un valor, rellena el área hasta esa línea base. |
policy | str | "gap" | Muestras no finitas: gap corta la línea, drop las omite y error lanza un error. |
color / width | Color | None / float | None | None | Sustituyen el trazo del tema. |
from gaanim import CYAN, Axis, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
plane = scene.viz.cartesian_2d(
Axis.linear(0, 30).ticks(5).label("tiempo (s)"),
Axis.linear(-0.4, 0.4).ticks(0.2).label("aceleración (g)"),
width=12,
height=4.75,
)
curve = plane.plot_data(times, accel, color=CYAN, width=0.05)
scene.play([plane.animate.create().duration(0.85), curve.animate.create().duration(2.2)])radius (unidades de escena) en las coordenadas de datos; view_to mueve los centros sin deformarlos. Úsalo para destacar muestras sobre una curva de plot_data.peaks = plane.scatter_data(peak_times, peak_values, radius=0.07, color=GOLD)
scene.play([peaks.animate.fade_in()])Valores reactivos
Magnitudes que conducen otras propiedades. Rust resuelve un snapshot numérico estable para cada instante y llama después a las funciones Python declaradas, así que el seek es exacto.
Elige el tipo según su papel:
Parameter: magnitud invisible que conduce otras propiedades.Variable: magnitud que además se muestra comoetiqueta = valor.Readout: muestra una cantidad derivada sin ser su fuente.computed(...): escalar calculado a partir de otros.
Las funciones reciben primero las coordenadas y después los valores de inputs=[...], en el orden declarado. Pueden usar math, funciones auxiliares y control de flujo, pero deben ser puras y devolver un escalar finito. Los setters absolutos (move_to, rotate_to, scale_to, opacity y sus variantes 3D) aceptan estas fuentes directamente.
point_on_curve, always_redraw_arc…) y setters; cada objeto conducido necesita su propia animación de entrada. Valores no finitos lanzan ValueError.import math
from gaanim import Axis, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
amplitude = scene.viz.parameter(1.0)
axes = scene.viz.cartesian_2d(Axis.linear(-4, 4), Axis.linear(-2, 2))
curve = axes.plot(lambda x, a: a * math.sin(x), inputs=[amplitude])
scene.play([axes.animate.create(), curve.animate.write(), amplitude.animate.set(2.0).duration(1.2)])parameter.animate.set(value) devuelve un Anim con duración y easing configurables para scene.play.callback(actual, dt, transcurrido) -> valor. Sin reset ni fixed_dt, calcula el valor a partir de transcurrido, como en el ejemplo: un valor que acumula dt vuelve al inicial al hacer seek o capturar fotogramas sueltos. Para simulaciones con estado, pasa reset y un fixed_dt positivo: Gaanim llama a reset() y repite los pasos fijos al hacer seek y al exportar. Las simulaciones de objetos de paso fijo se ejecutan antes, así que el callback ve su estado del mismo fotograma. Resultados no finitos lanzan ValueError.phase = scene.viz.parameter(0.0)
phase.add_updater_fn(lambda value, dt, elapsed: 2.0 * elapsed)
hand = scene.geometry.line((0, 0), scene.geometry.polar_point((0, 0), 1.5, phase))
scene.wait(1)(times, values) de forma nativa: el valor es offset + scale * muestra como función pura del tiempo, sin callbacks. times es relativo al cursor donde haces la llamada; interpolation es "linear" o "step".level = scene.viz.parameter(0.0)
level.drive_from_samples([0, 0.5, 1.0, 1.5], [0, 1, 0.4, 0.8], scale=2.0)
readout = scene.viz.readout(level, label="$h$", unit="m")
scene.wait(1.5)inputs, que pueden incluir otros Computed. Las dependencias compartidas reutilizan su caché para el mismo snapshot. Sigue siendo escalar: no devuelve colores, texto ni geometría. Envolver un parámetro de otra escena no permite usarlo aquí.radius = scene.viz.parameter(1.0)
area = computed(lambda r: math.pi * r * r, inputs=[radius])
doubled = computed(lambda value: 2 * value, inputs=[area])
scene.viz.readout(doubled, label="2A")scene.time.bob = scene.geometry.dot(0.15).fill(GOLD)
bob.move_to(0, computed(lambda t: math.sin(3 * t), inputs=[scene.viz.time]))
scene.wait(2)etiqueta = valor unidad. Acepta las mismas operaciones que Parameter. Sus partes label, equals, number y unit son Drawable que se estilizan por separado; todos los términos usan font_size (0.48 por defecto) y comparten línea base. color pinta todo, también el número al actualizarse.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
initial | float | obligatorio | Valor inicial. |
label | str | obligatorio | Etiqueta antes del signo igual; admite matemática $…$. |
format | str | ".2f" | Especificación de formato de Python para un float, como en format(valor, spec): relleno y alineación, signo, z, #, 0, ancho, agrupación , o _, precisión y tipo f, F, e, E, g, G o %. "05.2f" muestra 00.07. Los tipos de enteros o texto (d, x, s…) y las especificaciones mal formadas lanzan ValueError; para enteros usa ".0f". El relleno con espacios no se ve, porque el número se alinea a la derecha por su tinta: para reservar sitio usa reserve. |
prefix / suffix / unit | str / str / str | None | "" / "" / None | Afijos y unidad visibles. |
decimal_separator | str | "." | "," muestra 3,14 y convierte la agrupación , en .. |
font / weight | str | None / int | None | None / None | Familia y peso (1 a 1000) del número y de la etiqueta y la unidad en texto plano; una etiqueta $…$ conserva la fuente matemática. Sin ellos, el número usa la fuente matemática del tema. Un peso fuera de rango lanza ValueError. |
from gaanim import RED, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
k = scene.viz.variable(10, label="$k$", format=".0f", color=RED)
scene.play([k.animate.create(), k.animate.set(100).duration(1.5)])color usa el color de texto del tema. Dentro de un layout, su celda mide lo que el texto más ancho que mostrará: el de los valores a los que se fijan o animan sus parámetros (el percent de una encuesta llega a 100) y el de reserve. El número queda alineado a la derecha en ese espacio, así que sus vecinos no se mueven mientras cuenta. variable también acepta reserve.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
source | número | Parameter | Variable | Computed | callable | obligatorio | Escalar o función pura cuyos argumentos son inputs. |
inputs | Sequence | () | Dependencias en orden. |
invalid | str | "invalid" | Texto para evaluaciones inválidas o no finitas. |
label / format / prefix / suffix / unit / decimal_separator / font / weight | — | — | Igual que en variable; un separador que sea dígito, signo, espacio, e o % lanza ValueError. |
spacing | float | None | None | Espacio entre etiqueta, =, número y unidad, en unidades de escena. Por defecto, un espacio fino que crece con font_size (0.1 con el tamaño por defecto). prefix y suffix van pegados al número: suffix=" m" conserva su espacio. |
reserve | float | None | None | Valor cuyo texto cabe en la celda de un layout, además de los valores que toman sus parámetros. Úsalo con conteos en vivo sin máximo conocido, como votes (reserve=999). Un valor no finito lanza ValueError. |
import math
from gaanim import Scene
scene = Scene(frame=(16, 9), background="#0f172a")
radius = scene.viz.parameter(1.0)
area = scene.viz.readout(lambda r: math.pi * r**2, inputs=[radius], label="$A$", format=".2f", unit="m²")
scene.play([area.animate.create(), radius.animate.set(3.0).duration(1.5)])Readout y Variable que se estilizan por separado (number siempre existe; las demás valen None si no se pidieron). Variable añade current, set, animate, add_updater_fn y remove_updater, con el mismo comportamiento que en Parameter.speed = scene.viz.readout(3.2, label="$v$", unit="m/s")
speed.number.fill(GOLD)anchor, su centro va a (x, y); con un Anchor, ese punto de su caja. Con un TextAnchor la coloca por su línea base, alineada con un Text: TextAnchor.BASELINE_LEFT deja fijo su comienzo aunque el número cambie de ancho. Con un TextAnchor hacen falta x e y, que pueden ser escalares reactivos.scene.text("Carga").move_to(-4, 0, TextAnchor.BASELINE_LEFT)
scene.viz.readout(12.5, label="$P$", unit="kN").move_to(-2, 0, TextAnchor.BASELINE_LEFT)Contador rodante
Cifras que giran como un cuentakilómetros, para dinero, marcadores y cuentas regresivas.
min_digits reserva posiciones para que no crezca durante un acarreo. Con pad=" " esas posiciones quedan vacías: el contador ocupa siempre lo mismo, el prefijo no se desplaza y una frase con el valor final queda centrada para todos los valores. En modo continuous, las ruedas se asientan durante el primer y el último 15 % de cada animación del valor. Opciones inválidas lanzan ValueError; si un driver sale del rango, se muestra una raya.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | float | 0.0 | Valor inicial finito; su magnitud por 10**decimals debe ser menor que 1e15. |
decimals / min_digits | int | 0 / 1 | De 0 a 6 decimales y de 1 a 15 posiciones enteras reservadas; la suma no supera 15. |
pad | str | "0" | Relleno de las posiciones reservadas sin usar: "0" dibuja ceros iniciales y " " deja celdas vacías del mismo ancho, sin dibujar sus separadores de miles. |
group_separator / decimal_separator | str | "" / "." | Cero o un carácter, y un carácter distinto. |
prefix / suffix / show_plus | str / str / bool | "" / "" / False | Afijos de una línea (256 bytes UTF-8 en total) y signo positivo visible. |
font_family / weight | str | None / int | None | None | Familia y peso (1 a 1000), resueltos como en scene.text; sin familia hereda la de cuerpo. |
font_size / digit_spacing / line_height | float | 0.75 / 0.02 / 1.25 | Tamaño, espacio entre celdas y altura de la ventana como múltiplo de la altura de las cifras (al menos 1). |
mode | str | "odometer" | odometer arrastra las ruedas superiores durante la última unidad antes del acarreo; continuous gira cada rueda a la velocidad de su posición. |
direction | str | "up" | up o down para magnitudes crecientes; al disminuir se invierte. |
color | Color | None | None | Color de cifras, signo y afijos; sin él, el del tema. |
from gaanim import Easing, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
counter = scene.viz.rolling_number(
98, min_digits=4, prefix="$ ", group_separator=",",
).move_to(0, 0)
scene.play([counter.count_to(1250, duration=3).easing(Easing.SMOOTH)])
scene.play([counter.animate.set(500).duration(2)])
designs = scene.viz.rolling_number(0, min_digits=8, pad=" ", group_separator=" ").move_to(0, -1.5)
scene.play([designs.count_to(14_000_605, duration=3)])
counter.set(0) # Corte reversible en el cursor actual.
scene.wait(0.5)
scene.render()scene.play, compatible con easing y retardo. El destino se usa tal cual: una fracción de la unidad mínima deja la rueda entre dos cifras. snap=True redondea al valor representable más cercano, así que termina asentado y current coincide con lo visible.mean = scene.viz.rolling_number(0, decimals=1)
scene.play([mean.count_to(61.7956, snap=True)]) # Termina en 61.8.snap como count_to.TextAnchor.BASELINE_LEFT, BASELINE_CENTER o BASELINE_RIGHT, la línea base de las cifras queda en y, para alinearlo con texto; un Anchor usa la ventana completa, con el margen de line_height.number = scene.viz.rolling_number(2, min_digits=2, font_size=1)
number.move_to(-5, 1.12, TextAnchor.BASELINE_LEFT)
scene.text("Objetivos", size=1).move_to(-3.5, 1.12, TextAnchor.BASELINE_LEFT)visual es el objeto dibujado: usa counter.visual.animate para posición, opacidad o entradas (sin entrada, el contador es visible desde su declaración). parameter es el escalar subyacente, reutilizable en computed, lecturas o drive_from_samples. animate.set(value) anima el valor como en un Parameter. move_to, fill, opacity y set devuelven el contador; otros métodos heredados pueden devolver su Drawable.Anillos de progreso y temporizadores
Un arco que se llena en el sentido del reloj desde las 12, con una pista tenue detrás y un contador rodante en el centro. Todo sigue a un único Parameter.
animate.set(...); la geometría depende solo del valor actual, así que reproducción, seeks y exportación coinciden. arc, track y label dan acceso a cada parte. Tamaños no positivos o un valor no finito lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | float | 0.0 | Progreso inicial; el anillo muestra value recortado a [0, 1]. |
radius / width | float | 1.0 / 0.12 | Radio del anillo y grosor del trazo, en unidades de escena. |
color | Color | None | None | Color del arco; sin él, el acento del tema. |
track / track_color | bool / Color | None | True / None | Dibuja un círculo tenue completo detrás del arco, con otro color si se indica. |
label / decimals | bool / int | True / 0 | Muestra el porcentaje con un contador rodante de 0 a 6 decimales. |
label_color / font_size | Color | None / float | None / 0.5 | Color y tamaño del porcentaje; sin color, el del texto. |
from gaanim import Scene
scene = Scene(frame=(16, 9), background="#0f172a")
ring = scene.viz.progress_ring(0.0, radius=1.6, width=0.18)
scene.play([ring.animate.set(0.75).duration(1.5)])
scene.render()
ProgressRing que empieza lleno, con los segundos restantes rodando en el centro. count_down() lo vacía a velocidad constante. El resto de opciones son las de progress_ring. Unos seconds no positivos lanzan ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
seconds | float | obligatorio | Duración de la cuenta; también es el máximo del anillo. |
from gaanim import Scene
scene = Scene(frame=(16, 9), background="#0f172a")
timer = scene.viz.countdown(3, radius=1.6, width=0.18)
scene.play([timer.count_down()])
scene.render()
countdown(10) dura diez segundos reales. Una duración negativa lanza ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
duration | float | None | None | Segundos de la animación; por defecto, el valor actual. |
Anim — El valor bajando hasta cero con easing lineal.parameter es el escalar que mueve el arco y la etiqueta, reutilizable en computed o lecturas. maximum es 1.0 en un anillo y los segundos en una cuenta atrás. set fija el valor al instante (un corte reversible tras la declaración) y animate.set(value) lo anima. visual es el grupo de las tres partes: usa visual.animate para moverlo o desvanecerlo. move_to, shift_by y opacity devuelven el anillo. La etiqueta se asienta en cifras enteras: redondea el porcentaje (la cuenta atrás redondea los segundos hacia arriba) y gira a la cifra siguiente cuando el valor la cruza, así que un valor como 1/3 muestra 33% sin quedarse entre dos cifras.Carrera de barras
Una bar chart race: barras ordenadas por valor que crecen, se adelantan unas a otras y salen del ranking. Todo es función pura de una única posición de fotograma clave, así que un seek muestra lo mismo que la reproducción continua.
top se desvanecen por debajo del último puesto y la barra más larga ocupa siempre todo el ancho disponible. Los nombres van en una columna a la izquierda y un contador rodante (rolling_number) sigue el extremo de cada barra. Si todas las etiquetas son números enteros, como años, el ticker rueda entre ellos; si no, cambia de texto con un fundido. Datos vacíos o no finitos, nombres repetidos, un value_format no admitido o tamaños no positivos lanzan ValueError; otra forma de frames, TypeError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
frames | Mapping[Any, Mapping[str, float]] | Sequence[tuple[Any, Mapping[str, float]]] | obligatorio | Cada etiqueta de fotograma clave (un año, una fecha…) con los {nombre: valor} de ese instante, en orden. Un nombre ausente vale cero en ese fotograma. |
top | int | 10 | Puestos visibles; el líder arriba. |
rank_smoothing | float | 0.3 | Fotogramas clave (de 0 a 2) que dura el intercambio de dos barras, centrado en el instante en que se cruzan y acortado para que termine antes de que esas dos barras vuelvan a cruzarse; con 0 cambian de puesto al instante. |
value_format | str | "{:,.0f}" | Campo de formato de Python con texto opcional alrededor: "{}", "{:,.0f}", "{:_.2f}" o "${:,.1f} M", hasta 6 decimales. |
width / height | float | 10.0 / 6.0 | Tamaño de toda la carrera, centrada en su origen, en unidades de escena. |
label_width / bar_gap | float | None / float | None / 0.18 | Ancho de la columna de nombres (por defecto el 22% de width) y fracción vacía de cada puesto. Un nombre más ancho que la columna se dibuja más pequeño, hasta que cabe. |
colors | Mapping[str, Color] | Sequence[Color] | None | None | Colores por nombre o una lista que vuelve a empezar como una paleta; el resto toma la paleta del tema (Theme(palette=...)). |
label_color / font_size | Color | None / float | None | None / None | Color y tamaño de nombres y valores; por defecto el color de texto del tema y el 55% del grosor de la barra, sin pasar del 12% de height para que las carreras de pocos puestos sigan legibles. |
ticker / ticker_size / ticker_color | bool / float | None / Color | None | True / None / None | Etiqueta del fotograma clave abajo a la derecha, por defecto al 15% de height y con el color atenuado del tema. |
from gaanim import Scene
scene = Scene(frame=(16, 9), background="#0f172a")
frames = {
2020: {"Ana": 12, "Luis": 30, "Marta": 18, "Noé": 5},
2021: {"Ana": 35, "Luis": 34, "Marta": 22, "Noé": 20},
2022: {"Ana": 48, "Luis": 40, "Marta": 51, "Noé": 38},
2023: {"Ana": 60, "Luis": 44, "Marta": 58, "Noé": 71},
}
race = scene.viz.bar_race(frames, top=3, width=12, height=6)
scene.play([race.animate.play().duration(4)])
scene.render()
race.animate.play() avanza desde la posición actual hasta el último fotograma clave a ritmo constante, un segundo por fotograma pendiente salvo que encadenes .duration(segundos). to(position) va a una posición concreta: 0 es el primer fotograma clave y las posiciones fraccionarias quedan entre dos. Una posición fuera de [0, frame_count - 1] lanza ValueError.parameter es la posición de fotograma clave, reutilizable en computed o lecturas. bar(name) devuelve la fila de una barra (barra, nombre y valor), que se mueve entre puestos y se desvanece al salir del top; su propia opacidad queda libre, así que race.bar(name).animate.opacity(0.3) la atenúa además de ese desvanecimiento. Un nombre desconocido lanza KeyError. set salta a una posición (un corte reversible tras la declaración). visual es el grupo de todas las filas y el ticker; move_to, shift_by y opacity devuelven la carrera.Recta numérica, plano polar y plano complejo
Espacios tipados de una dimensión, polares y complejos.
axis_visible, ticks, numbers y labels controlan cada componente; los componentes ocultos siguen disponibles como capas vacías y la conversión de datos sigue activa.line = scene.viz.number_line(Axis.linear(0, 10).ticks(1), length=10)PointRef reactivo que sigue a la recta cuando se mueve, rota o escala. Las escalas categóricas rechazan valores reactivos con ValueError.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
value | float | Parameter | Variable | Computed | obligatorio | Valor convertido con la escala de la recta. |
normal_offset | float | fuente reactiva | None | None | Desplazamiento perpendicular en unidades locales. |
reveal reactivo puede compartir el mismo escalar que un punto móvil sin desfases de longitud de arco.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
function | Callable[..., float] | obligatorio | Recibe la coordenada y después inputs. |
normal_scale | float | 1.2 | Distancia local para una salida igual a 1. |
reveal | float | fuente reactiva | None | None | Extremo visible exacto, en coordenadas de datos. |
import math
from gaanim import Axis, Scene, computed
scene = Scene(frame=(16, 9), background="#0f172a")
theta = scene.viz.parameter(0.0)
line = scene.viz.number_line(
Axis.linear(0, 3 * math.pi).ticks(math.pi).numbers("pi", denominator=1),
length=9.5,
)
curve = line.function(lambda t: math.sin(t), normal_scale=1.2, reveal=theta)
point = scene.geometry.dot(0.1).follow(
line.point_ref(theta, normal_offset=computed(lambda t: 1.2 * math.sin(t), inputs=[theta]))
)
scene.play([line.animate.create(), curve.animate.fade_in().duration(0.01), point.animate.fade_in()])
scene.play([theta.animate.set(3 * math.pi).duration(4)])axis, ticks, numbers y labels.radius con angle_divisions radios. rings y spokes heredan grid si se omiten; numbers afecta a los valores radiales y labels, al título radial.polar = scene.viz.polar(Axis.linear(0, 2).ticks(0.5).label("r"), radius=3)
rose = polar.plot(lambda angle: 2 * abs(math.cos(3 * angle))).stroke(GOLD, 0.04)r = f(θ) sobre domain (una vuelta completa por defecto).grid, axes, numbers y labels.Re e Im; los interruptores y su precedencia son los de cartesian_2d, y devuelve el mismo tipo de espacio.plane = scene.viz.complex(labels=False)
z = scene.geometry.dot(0.1).fill(GOLD).at_coordinate(plane.coord(1, 1))Gráficos declarativos
ChartSpec es una receta inmutable: datos, marca, codificaciones, ejes y guías. scene.viz.chart(spec) la materializa en capas y chart.animate.to(otra) anima el paso de una receta a otra.
from gaanim import Axis, ChartSpec, Field, Guide, Scale, Scene, Value
scene = Scene(frame=(16, 9), background="#0f172a")
spec = (
ChartSpec({
"id": ["a", "b", "c", "d"],
"x": [-2, -0.5, 1, 2.5],
"y": [1, 3, 2, 3.5],
"group": ["A", "B", "A", "B"],
}, key="id")
.mark("point")
.encode(x="x", y="y", color=Field("group", scale=Scale.category()), size=Value(0.12))
.axes(x=Axis.linear(-3, 3).ticks(1).label("x"), y=Axis.linear(0, 4).ticks(1).label("y"))
.guides(color=Guide.legend(title="Grupo"))
)
chart = scene.viz.chart(spec)
scene.play([chart.layer("axes").animate.create().duration(0.8)])
scene.play([chart.layer("marks").animate.fade_in(), chart.layer("guides").animate.fade_in()], duration=0.6)
scene.render()
| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
data | mapping | dataframe | DataTable | DataSource | obligatorio | Se copia al instante como instantánea inmutable propia. |
key | str | None | None | Columna de identidad estable para las transiciones; nulos o duplicados lanzan un error. |
base = ChartSpec({"category": ["a", "b", "c"], "time": [0, 1, 2], "value": [3, 1, 2]})
bars = base.mark("bar").encode(x="category", y="value")
points = base.mark("point").encode(x="time", y="value")point, line, step, area, bar, histogram, box, violin, error_bar, heatmap, surface o arc. Las opciones dependen de la marca: las barras aceptan width (fracción de la banda en ejes categóricos), label_position ("outside" o "inside"), label_offset y label_color; los puntos, radius. arc dibuja una tarta sin ejes, una porción por fila según su parte del total de theta, en el sentido del reloj desde las 12; acepta inner_radius (fracción del radio que queda vacía, en [0, 1): un donut si es mayor que 0), pad_angle (radianes vacíos entre porciones), start_angle (radianes en sentido antihorario desde el eje x; math.pi / 2 por defecto), radius (unidades de escena) y label_color. Las marcas rellenas (bar, histogram, area, point, box, violin y las celdas de heatmap) se dibujan sin borde; para trazarlo, usa chart.layer("marks").stroke(...).bars = (
ChartSpec({"method": ["one", "two"], "elapsed": [40, -10], "kind": ["new", "old"]})
.mark("bar", width=0.72, label_position="outside", label_offset=0.2)
.encode(
x="method", y="elapsed", label="elapsed",
color=Field("kind", scale=Scale.category().colors([BLUE, GOLD])),
)
)x, y, z, color, size, opacity, label, theta, radius). Cada canal acepta un nombre de columna, Field(columna, scale=...) o Value(constante). En point, size es el radio en unidades de escena (0.06 por defecto); un Field numérico lo reparte entre 0.03 y 0.12 con área proporcional. color y opacity con Field valen por fila; en line, step y area, un color categórico dibuja una serie por categoría. theta y radius son de arc: theta (obligatorio, no negativo; un valor ausente no ocupa sitio) da a cada fila su parte del círculo, y un campo radius dibuja un gráfico de rosa, con cada porción desde el radio interior hacia el exterior en proporción a su valor.color, size y opacity, derivadas de sus escalas.None.Field. La misma escala gobierna posiciones, colores, leyendas y barras de color. category sin valores los infiere en orden de aparición.color = Field("temperature", scale=Scale.symlog((-100, 100), threshold=1))
kind = Field("kind", scale=Scale.category(["new", "old"]).colors(["#38bdf8", "#f97316"]))Palette, el nombre de una paleta ("okabe_ito") o colores (Color, cadenas CSS/hex o tuplas RGB(A), incluso mezclados). Colores inválidos, nombres desconocidos o una lista vacía lanzan ValueError. En un campo de texto, los colores se reparten entre las categorías por turno y vuelven a empezar como una paleta; sin ellos, las categorías toman la paleta del tema. En un campo numérico forman una rampa. En un heatmap, los colores y el dominio de la escala del canal color sustituyen a la paleta heatmap del tema solo en ese gráfico, y bands= cuantiza sobre ellos.Value(BLUE) da un único color a toda la serie.legend lista cada categoría con una muestra de su color, en el orden de Scale.category o de aparición; colorbar es la guía continua; disabled oculta la guía de un canal.replace o append, construye un ChartSpec nuevo para capturar la nueva versión inmutable; version cuenta los cambios. DataTable(columns) es la tabla inmutable equivalente, con columns y len().source = DataSource({"x": [0, 1], "y": [2, 3]})
before = ChartSpec(source).mark("line").encode(x="x", y="y")
source.append({"x": [2], "y": [5]})
after = ChartSpec(source).mark("line").encode(x="x", y="y")Chart con capas semánticas por lotes: las marcas se agrupan por estilo resuelto en lugar de crear una entidad por registro.Drawable: marks, axes, grid, guides o labels. La capa axes reúne rejilla, ejes, marcas, números y títulos, así que chart.layer("axes").animate.create() la mantiene oculta hasta su entrada. Las tres últimas son opcionales: grid existe si los ejes dibujan rejilla, guides si la especificación tiene leyenda o título de guía (ChartSpec.guides) y labels en gráficos de barras o de arcos con el canal label. La capa axes de un gráfico de arcos está vacía. Pedir una que el gráfico no tiene lanza ValueError con las capas que sí tiene.chart = scene.viz.chart(ChartSpec({"x": [0, 1, 2], "value": [18, 42, 31]}).mark("bar").encode(x="x", y="value"))
scene.play([chart.layer("axes").animate.create(), chart.layer("marks").animate.fade_in()])chart.animate.grow(): una manecilla gira en el sentido del reloj desde el ángulo inicial, acelerando y frenando, y cada porción se abre en su sitio cuando la manecilla la cruza. La etiqueta de una porción aparece desde que la manecilla pasa por su centro hasta que la porción se completa; las guías aparecen a lo largo de toda la entrada. En otro tipo de gráfico lanza ValueError.Composition — Las animaciones de la entrada, en paralelo.sales = {"region": ["North", "South", "East"], "sales": [42, 28, 18]}
spec = ChartSpec(sales, key="region").mark("arc", inner_radius=0.55).encode(theta="sales", color="region", label="region")
chart = scene.viz.chart(spec)
scene.play([chart.animate.grow()], duration=1.5)
later = ChartSpec({"region": ["North", "South", "West"], "sales": [30, 35, 20]}, key="region")
scene.play([chart.animate.to(later.mark("arc", inner_radius=0.55).encode(theta="sales", color="region", label="region"))])chart.animate.to(...). Entre gráficos 2D, las marcas, ejes y etiquetas que se corresponden se transforman y el resto se funde; al terminar, el gráfico de destino sustituye al original. point, line y bar pueden pasar entre 2D y 3D (con un fundido), y heatmap y surface comparten rejilla. Entre dos gráficos de arcos, cada porción barre sus ángulos y radios hasta la porción con su clave (su fila con match_="index"); una porción sin pareja se cierra sobre su ángulo medio o se abre desde él. Nunca mueve la cámara global.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
target | ChartSpec | obligatorio | Receta de destino. |
match_ | str | "key" | "key" relaciona elementos por la columna de identidad (ambas recetas deben declararla); sin clave, pide "index". |
fallback | str | "error" | Con familias de marca incompatibles: "error" o "crossfade". |
target = spec.encode(z="height").axes(z=Axis.linear(-2, 2))
scene.play([
chart.animate.to(target).duration(1.4),
scene.camera.animate.look_at(eye=(8, 6, 8), target=(0, 0, 0)).duration(1.4),
])inspection_enabled indica si está activa. move_to, move_to_3d y scale_to devuelven el gráfico.Campos vectoriales
Un VectorField separa la función de sus representaciones: el mismo campo produce flechas, líneas de corriente, advección y partículas.
inputs. No dibuja por sí solo. dimensions vale 2 o 3 y evaluation, "python" en esta ruta.from gaanim import Axis, Scene
scene = Scene(frame=(16, 9), background="#0f172a")
plane = scene.viz.cartesian_2d(Axis.linear(-4, 4).ticks(1), Axis.linear(-3, 3).ticks(1))
field = plane.field(lambda x, y: (-y, x))
arrows = field.arrows(resolution=(18, 12), colormap="batlow")
streams = field.streamlines(seeds=(12, 8), max_time=3.5, colormap="vik")
scene.play([plane.animate.create(), arrows.animate.write()], duration=0.8)
scene.play([streams.animate.write().duration(0.8)])
scene.play(streams.flow(1.5, time_width=0.12))
scene.render()
ArrowVectorField (grupo retenido de astas y puntas) con drawable() y animate. Se valida todo antes de crear geometría.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
resolution | (int, int) | (int, int, int) | None | None | Muestras regulares por eje. |
min_length / max_length / length_scale | float | 0 / 0.28 (0.24 en 3D) / 1 | Límites y escala de longitud en unidades locales. |
width / tip_length / tip_width | float | 0.02 / auto / auto | Grosor del asta y tamaño de la punta. |
color / colormap | ColorLike | ColorMapLike | None | None / viridis | Excluyentes; el mapa usa la magnitud. |
color_range | (float, float) | None | None | Rango explícito de magnitudes. |
StreamLines con drawable() y animate.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
seeds | (int, int) | (int, int, int) | None | None | Resolución determinista de semillas candidatas. |
direction | str | "both" | forward, backward o both. |
tolerance / min_step / max_step | float | 1e-4 / 1e-5 / 0.1 | Control adaptativo Dormand–Prince RK45. |
max_time / max_length / max_steps | float | int | 3 / None / 10000 | Límites que hacen reproducible cada trayectoria. |
padding / separation | float | 0.05 / 0.035 | Margen del dominio y distancia mínima entre líneas. |
StreamLines.flow devuelve animaciones que desplazan resaltados más claros por las líneas sin recortar la geometría base; FlowParticles.flow devuelve un clip de advección por partícula. Son finitas y exactas al hacer seek: pasa la lista directamente a scene.play. Los tres grupos (StreamLines, ArrowVectorField y FlowParticles) ofrecen drawable() para layout y estilo y animate para sus entradas y salidas.seed (coordenadas de datos), con el mismo integrador. Devuelve un Anim finito por longitud de arco; no deforma la geometría del objeto.boat = scene.geometry.dot(0.12).fill(GOLD)
scene.play([field.advect(boat, (2, 0), duration=2)])count partículas con semillas de Halton deterministas (puntos en 2D, esferas en 3D). El FlowParticles resultante queda oculto hasta su primera entrada (create, write, fade_in, grow_from_center) y flow() devuelve un clip de advección por partícula.particles = field.particles(24, duration=2)
scene.play([particles.animate.fade_in()])
scene.play(particles.flow())Los colores y mapas explícitos de flechas y líneas tienen prioridad sobre las reglas plot del tema, que sigue controlando el fondo y los ejes.
Espacios 3D
Ejes 3D con rejillas por plano, superficies y curvas paramétricas. Combínalos con la cámara en perspectiva de Escena.
Cartesian3D (un CoordinateSpace3D) con capas estables. Las rejillas ocultas no emiten segmentos ni duplican las aristas de los ejes; las etiquetas miran a la cámara.| Nombre | Tipo | Predeterminado | Descripción |
|---|---|---|---|
x / y / z | Axis | obligatorio | Ejes lineales o temporales reutilizables. |
size | (float, float, float) | (10.0, 8.0, 6.0) | Tamaño positivo en el mundo; elígelo según la distancia de la cámara. |
xy_grid / xz_grid / yz_grid | bool | None | None | Visibilidad de cada plano; None hereda grid. |
x_axis / y_axis / z_axis, x_ticks / …, x_numbers / …, x_labels / … | bool | None | None | Eje, marcas, números y título de cada eje, con el prefijo x_, y_ o z_; None hereda el interruptor global. |
world = scene.viz.cartesian_3d(
Axis.linear(-3, 3).ticks(1).label("x"),
Axis.linear(-3, 3).ticks(1).label("y"),
Axis.linear(-2, 2).ticks(1).label("z"),
size=(6, 6, 4), xz_grid=False, yz_grid=False,
)z = f(x, y) muestreada en una rejilla de resolution, con inputs reactivas como plot. Los vértices se colorean por altura, de la más baja a la más alta, con color_map: un ColorMap o el nombre de un mapa integrado, como "viridis" o "plasma". Sin él, usa una rampa de azul, verde y rojo.saddle = world.surface(lambda x, y: 0.1 * (x * x - y * y), resolution=(32, 24))t → (x, y, z) sobre domain con samples muestras e inputs reactivas.grid, axes, ticks, numbers o labels. move_to_3d, scale_to, data_to_local y local_to_data completan el espacio.Límites
La inspección interactiva solo existe en la vista previa. La exportación interactiva, los dashboards, las facetas, los volúmenes y las isosuperficies aún no forman parte de esta API. La animación de cámara es global, explícita y componible.