diff --git a/.gitignore b/.gitignore index 0906539..fc7d8ea 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ .vscode/ debug.test +example/debug +example/out.png diff --git a/example/data.txt b/example/data.txt new file mode 100644 index 0000000..7e1af8a --- /dev/null +++ b/example/data.txt @@ -0,0 +1,9 @@ +5,7 +2,4 +35,35 +3,6 +18,20 +20,30 +25,28 +30,32 +10,12 \ No newline at end of file diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..4ab97ad --- /dev/null +++ b/example/main.go @@ -0,0 +1,232 @@ +package main + +import ( + "bufio" + "fmt" + "image/color" + "log" + "os" + + "bitbucket.org/differenttravel/interval" + "gonum.org/v1/plot" + "gonum.org/v1/plot/palette" + "gonum.org/v1/plot/plotter" + "gonum.org/v1/plot/vg" + "gonum.org/v1/plot/vg/draw" + "gonum.org/v1/plot/vg/vgimg" +) + +const ( + MinX = 0 + MaxX = 40 +) + +func main() { + filename := "data.txt" + xys, err := readData(filename) + if err != nil { + log.Fatalf("could not read %s: %v", filename, err) + } + intervals := initIntervals(xys) + err = plotData("out.png", intervals) + if err != nil { + log.Fatalf("could not plot data: %v", err) + } +} + +type xy struct{ x, y int } + +func readData(path string) ([]xy, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var xys []xy + + // read line by line using a scanner (because we don't know if the file will be huge) + s := bufio.NewScanner(f) + for s.Scan() { + var x, y int + _, err := fmt.Sscanf(s.Text(), "%d,%d", &x, &y) + if err != nil { + log.Printf("discarding bad data point %v: %v", s.Text(), err) + } + xys = append(xys, xy{x, y}) + } + if err := s.Err(); err != nil { + return nil, fmt.Errorf("could not scan: %v", err) + } + return xys, nil +} + +func initIntervals(xys []xy) interval.Intervals { + intervals := interval.NewIntervals(MinX, MaxX) + for _, xy := range xys { + intervals.Add(&interval.Interval{Low: xy.x, High: xy.y}) + } + intervals.Sort() + return intervals +} + +func convertToPlotterXYs(intervals []*interval.Interval) plotter.XYs { + pxys := plotter.XYs{} + for _, intvl := range intervals { + pxys = append(pxys, struct{ X, Y float64 }{X: float64(intvl.Low), Y: float64(intvl.High)}) + } + return pxys +} + +func alignPlots(plotItems []*plot.Plot) *vgimg.Canvas { + rows, cols := len(plotItems), 1 + plots := make([][]*plot.Plot, rows) + for j := 0; j < rows; j++ { + plots[j] = make([]*plot.Plot, cols) + for i := 0; i < cols; i++ { + p := plotItems[j] + + // make sure the horizontal scales match + p.X.Min = MinX + p.X.Max = MaxX + + plots[j][i] = p + } + } + + img := vgimg.New(vg.Points(512), vg.Points(float64(128*rows))) + dc := draw.New(img) + + t := draw.Tiles{ + Rows: rows, + Cols: cols, + } + + canvases := plot.Align(plots, t, dc) + for j := 0; j < rows; j++ { + for i := 0; i < cols; i++ { + if plots[j][i] != nil { + plots[j][i].Draw(canvases[j][i]) + } + } + } + return img +} + +func createFileFromCanvas(path string, img *vgimg.Canvas) error { + f, err := os.Create(path) + if err != nil { + return fmt.Errorf("could not create %s: %v", path, err) + } + + png := vgimg.PngCanvas{Canvas: img} + if _, err := png.WriteTo(f); err != nil { + return fmt.Errorf("could not write to %s: %v", path, err) + } + + if err := f.Close(); err != nil { + return fmt.Errorf("could not close %s: %v", path, err) + } + return nil +} + +func createPlot(title string, xys plotter.XYs) (*plot.Plot, error) { + p, err := plot.New() + if err != nil { + return nil, fmt.Errorf("could not create plot: %v", err) + } + + // Draw a grid behind the data + p.Add(plotter.NewGrid()) + p.Title.Text = title + p.HideY() + // p.X.Label.Text = "values" + p.X.Padding = vg.Length(5) + p.Y.Padding = vg.Length(20) + plotIntervals(p, xys) + return p, nil +} + +func plotData(path string, intervals interval.Intervals) error { + plots := []*plot.Plot{} + + // create Intervals plot + xysIntervals := convertToPlotterXYs(intervals.Get()) + p1, err := createPlot("Intervals", xysIntervals) + if err != nil { + return fmt.Errorf("could not create plot: %v", err) + } + plots = append(plots, p1) + + // create Gaps plot + xysGaps := convertToPlotterXYs(intervals.Gaps()) + p2, err := createPlot("Gaps", xysGaps) + if err != nil { + return fmt.Errorf("could not create plot: %v", err) + } + plots = append(plots, p2) + + // create Overlapped `plot + xysOverlapped := convertToPlotterXYs(intervals.Overlapped()) + p3, err := createPlot("Overlapped", xysOverlapped) + if err != nil { + return fmt.Errorf("could not create plot: %v", err) + } + plots = append(plots, p3) + + // create Merged plot + xysMerged := convertToPlotterXYs(intervals.Merge()) + p4, err := createPlot("Merged", xysMerged) + if err != nil { + return fmt.Errorf("could not create plot: %v", err) + } + plots = append(plots, p4) + + // join all plots, align them + canvas := alignPlots(plots) + err = createFileFromCanvas("out.png", canvas) + if err != nil { + return err + } + return nil +} + +func plotIntervals(p *plot.Plot, xys plotter.XYs) error { + var ps []plot.Plotter + colors := getColors() + numColors := len(colors) + for i, xy := range xys { + label := fmt.Sprintf("(%v,%v)", xy.X, xy.Y) + pXYs := plotter.XYs{{xy.X, float64(i)}, {xy.Y, float64(i)}} + color := colors[i%numColors] + + s, err := plotter.NewScatter(pXYs) + + if xy.X != xy.Y { + l, err := plotter.NewLine(pXYs) + l.Color = color + l.Width = vg.Points(10) + if err != nil { + return fmt.Errorf("could not create a new line: %v", err) + } + ps = append(ps, l) + p.Legend.Add(label, l) + } else { + s.Color = color + p.Legend.Add(label, s) + } + + if err != nil { + return fmt.Errorf("could not create a new scatter: %v", err) + } + ps = append(ps, s) + + } + p.Legend.Left = false + p.Add(ps...) + return nil +} + +func getColors() []color.Color { + palette := palette.Rainbow(10, 0, 1, 1, 1, 1) + return palette.Colors() +} diff --git a/get.go b/get.go new file mode 100644 index 0000000..4085c92 --- /dev/null +++ b/get.go @@ -0,0 +1,6 @@ +package interval + +func (intvls *intervals) Get() []*Interval { + intvls.Sort() + return intvls.Intervals +} diff --git a/intervals.go b/intervals.go index c4517ac..3778b15 100644 --- a/intervals.go +++ b/intervals.go @@ -19,6 +19,9 @@ type Intervals interface { // HasGaps returns true if exists gaps for the introduced intervals between MinLow and MaxHigh HasGaps() bool + // Get first sorts (if necessary) and then returns the interval list + Get() []*Interval + // Gaps first sorts (if necessary) and then returns the interval gaps Gaps() []*Interval diff --git a/merge.go b/merge.go index c259e18..811fc9f 100644 --- a/merge.go +++ b/merge.go @@ -10,9 +10,12 @@ func (intvls *intervals) Merge() []*Interval { func (intvls *intervals) calculateMerged() []*Interval { intvls.Sort() list := []*Interval{} + if len(intvls.Intervals) == 0 { + return list + } + var lastLow int var lastHigh int - pendingToAdd := false for i, intvl := range intvls.Intervals { if i == 0 { lastLow = intvl.Low @@ -26,16 +29,12 @@ func (intvls *intervals) calculateMerged() []*Interval { if intvl.High > lastHigh { lastHigh = intvl.High } - pendingToAdd = true continue } list = append(list, &Interval{Low: lastLow, High: lastHigh}) lastLow = intvl.Low lastHigh = intvl.High - pendingToAdd = false - } - if pendingToAdd == true { - list = append(list, &Interval{Low: lastLow, High: lastHigh}) } + list = append(list, &Interval{Low: lastLow, High: lastHigh}) return list } diff --git a/overlap.go b/overlap.go index c7861d4..6629f17 100644 --- a/overlap.go +++ b/overlap.go @@ -24,8 +24,8 @@ func (intvls *intervals) calculateOverlapped() []*Interval { lastMaxHigh := math.MinInt64 for i, intvl := range intvls.Intervals { if i > 0 { - lowInBetween := isLowInBetween(lastMinLow, lastMaxHigh, intvl.Low, intvl.High) //inBetweenInclusive(lastMinLow, intvl.Low, intvl.High) || inBetweenInclusive(intvl.Low, lastMinLow, lastMaxHigh) - highInBetween := isHighInBetween(lastMinLow, lastMaxHigh, intvl.Low, intvl.High) //inBetweenInclusive(lastMaxHigh, intvl.Low, intvl.High) || inBetweenInclusive(intvl.High, lastMinLow, lastMaxHigh) + lowInBetween := isLowInBetween(lastMinLow, lastMaxHigh, intvl.Low, intvl.High) + highInBetween := isHighInBetween(lastMinLow, lastMaxHigh, intvl.Low, intvl.High) if lowInBetween || highInBetween { greaterLow := max(intvl.Low, lastMinLow) lowerHigh := min(intvl.High, lastMaxHigh)