cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Supersample only the pie image's edge and rim pixels, and find slices by binary search

Dami committed d794ca3
A pixel whose whole square lies inside one slice, clear of its edges
and the rim, gets the slice's color at once; one wholly outside the
disc is skipped. Slice edge directions are computed once, not per
sample. The images are bit for bit the same: a 120 x 40 cell pie drew
in 153 ms and now in 9 ms, which matters since images are redrawn on
every data change and resize. Finding the slice under a point is a
binary search, so a pie of 8192 slices draws as text in 0.45 ms, not
3.4 ms.
commit
d794ca3b51d738c9df1528b416c433d333e42d6b
parent
2daa81534f84bb1da002a4658ea5fb0c14a7ed76
author
Dami <dami.etoile@gmail.com>
2 files changed, +98 -31
Minternal/chart/image.go119++++++++++++++++++++++++------
Minternal/chart/pie.go10+--
M internal/chart/image.go +94 -25
@@ -211,42 +211,111 @@ func (p *barPlan) image(cv *canvas) {
211211 func (p *piePlan) image(cv *canvas) {212212 b := cv.Rect213213 cx, cy := float64(b.Dx())/2, float64(b.Dy())/2214- r := min(cx, cy) - 1215- const n = 4 // samples per pixel along each axis216- gap := 1.0 // pixels between slices217- counts := make([]float64, len(p.slices))214+ pr := newPieRaster(p, cx, cy, min(cx, cy)-1, 1)218215 for y := range b.Dy() {219216 for x := range b.Dx() {220- clear(counts)221- for sy := range n {222- for sx := range n {223- dx := (float64(x) + (float64(sx)+0.5)/n - cx) / r224- dy := (float64(y) + (float64(sy)+0.5)/n - cy) / r225- i := p.sliceAt(dx, dy)226- if i < 0 || len(p.slices) > 1 && p.nearEdge(i, dx, dy, gap/r) {227- continue228- }229- counts[i]++217+ if i, ok := pr.solid(float64(x)+0.5-cx, float64(y)+0.5-cy); ok {218+ if i >= 0 {219+ cv.blend(x, y, cv.pal.Series[i%Colors], 1)230220 }221+ continue231222 }232- for i, k := range counts {223+ for i, k := range pr.coverage(x, y) {233224 if k > 0 {234- cv.blend(x, y, cv.pal.Series[i%Colors], k/(n*n))225+ cv.blend(x, y, cv.pal.Series[i%Colors], k/(pieSamples*pieSamples))235226 }236227 }237228 }238229 }239230 }240231 241-// nearEdge reports whether a point of slice i lies within half of gap242-// (as a fraction of the radius) of the slice's straight edges.243-func (p *piePlan) nearEdge(i int, dx, dy, gap float64) bool {244- for _, f := range []float64{p.slices[i].from, p.slices[i].to} {245- a := f * 2 * math.Pi246- // The edge runs from the center toward (sin a, -cos a).247- ex, ey := math.Sin(a), -math.Cos(a)248- along := dx*ex + dy*ey249- if along > 0 && math.Abs(dx*ey-dy*ex) < gap/2 {232+// pieSamples is the samples per pixel along each axis.233+const pieSamples = 4234+235+// halfDiagonal is a little over the distance from a pixel's center to236+// its corners.237+const halfDiagonal = 0.7072238+239+// pieRaster decides which slices cover each pixel of a pie of radius r240+// pixels centered at cx, cy, leaving gap pixels between slices. Only241+// pixels near an edge or the rim are supersampled; any other lies wholly242+// in one slice, or outside the disc.243+type pieRaster struct {244+ p *piePlan245+ cx, cy float64246+ r, gap float64247+ // edges are unit vectors along each slice's straight edges, from the248+ // center, with y down.249+ edges [][2][2]float64250+ counts []float64251+}252+253+func newPieRaster(p *piePlan, cx, cy, r, gap float64) *pieRaster {254+ pr := &pieRaster{p: p, cx: cx, cy: cy, r: r, gap: gap, counts: make([]float64, len(p.slices))}255+ pr.edges = make([][2][2]float64, len(p.slices))256+ for i, s := range p.slices {257+ for k, f := range []float64{s.from, s.to} {258+ a := f * 2 * math.Pi259+ pr.edges[i][k] = [2]float64{math.Sin(a), -math.Cos(a)}260+ }261+ }262+ return pr263+}264+265+// solid reports the slice covering all of the pixel whose center is px,266+// py pixels from the pie's center, or -1 when the pixel is wholly267+// outside the disc; false means the pixel needs supersampling.268+func (pr *pieRaster) solid(px, py float64) (int, bool) {269+ d := math.Hypot(px, py)270+ switch {271+ case d-halfDiagonal > pr.r:272+ return -1, true273+ case d+halfDiagonal >= pr.r:274+ return 0, false275+ }276+ i := pr.p.sliceAt(px/pr.r, py/pr.r)277+ if len(pr.p.slices) == 1 {278+ return i, true279+ }280+ for _, e := range pr.edges[i] {281+ // The distance to the edge, a ray from the center.282+ dist := d283+ if px*e[0]+py*e[1] > 0 {284+ dist = math.Abs(px*e[1] - py*e[0])285+ }286+ if dist <= pr.gap/2+halfDiagonal {287+ return 0, false288+ }289+ }290+ return i, true291+}292+293+// coverage returns how many of pixel x, y's samples fall in each slice.294+func (pr *pieRaster) coverage(x, y int) []float64 {295+ const n = pieSamples296+ clear(pr.counts)297+ for sy := range n {298+ for sx := range n {299+ dx := (float64(x) + (float64(sx)+0.5)/n - pr.cx) / pr.r300+ dy := (float64(y) + (float64(sy)+0.5)/n - pr.cy) / pr.r301+ i := pr.p.sliceAt(dx, dy)302+ if i < 0 || len(pr.p.slices) > 1 && pr.nearEdge(i, dx, dy) {303+ continue304+ }305+ pr.counts[i]++306+ }307+ }308+ return pr.counts309+}310+311+// nearEdge reports whether a point of slice i, as a fraction of the312+// radius from the center, lies within half the gap of the slice's313+// straight edges.314+func (pr *pieRaster) nearEdge(i int, dx, dy float64) bool {315+ gap := pr.gap / pr.r316+ for _, e := range pr.edges[i] {317+ along := dx*e[0] + dy*e[1]318+ if along > 0 && math.Abs(dx*e[1]-dy*e[0]) < gap/2 {250319 return true251320 }252321 }
M internal/chart/pie.go +4 -6