In [1]:
import bqplot
import numpy as np
import pandas as pd
import ipywidgets as widgets
import pprint

In [10]:
# input coordinates and class, function to modify board state 
def shape_grid(num_shapes, shape_function, size, inp, **interact_params):
    sc_x = bqplot.scales.LinearScale(min=0, max=size)
    sc_y = bqplot.scales.LinearScale(min=0, max=size)
    ax_x = bqplot.axes.Axis(scale=sc_x)
    ax_y = bqplot.axes.Axis(scale=sc_y, tick_format='0.2f', orientation='vertical')
    ax_x.visible = False
    ax_y.visible = False
    
    lst_scatter_plts = []
    for i in range(num_shapes):
        lst_scatter_plts.append(
            bqplot.marks.Scatter(
                   scales={'x': sc_x, 'y': sc_y}, ))
#     layout = widgets.Layout(kw=dict(min_width='400px'))
    fig = bqplot.Figure(marks=lst_scatter_plts, axis=[ax_x,ax_y], min_aspect_ratio=1, 
                        max_aspect_ratio=1, fig_margin={'top':10, 'bottom':60, 'left':60, 'right':60})
    fig.layout.height = '600px'
    fig.layout.width = '600px'
    def wrapped(**interact_params):
        nonlocal inp
        lst_scatter_plts = []
        output = shape_function(size, inp, **interact_params)
        marks = fig.marks
        # keep track of which dictionaries in new output have been added so that we can add remaining in the end
        added = []
        empty_old_points = []
        for old_points in marks:
            old_point_match = False
            for dictionary in output:
                if dictionary['options']['marker'] == old_points.marker and dictionary['options']['color'] == old_points.colors[0]:
                    old_point_match = True
                    added.append(dictionary)
                    options = dictionary['options']
                    coords = dictionary['coords']
                    old_points.x = list(zip(*coords))[0]
                    old_points.y = list(zip(*coords))[1]
                    old_points.colors=[options['color']]
                    old_points.marker=options['marker']
            if not old_point_match:
                old_points.x = []
                old_points.y =[]
                empty_old_points.append(old_points)
        for dictionary in output:
            if dictionary not in added:
                old_points = empty_old_points.pop()
                options = dictionary['options']
                coords = dictionary['coords']
                old_points.x = list(zip(*coords))[0]
                old_points.y = list(zip(*coords))[1]
                old_points.colors=[options['color']]
                old_points.marker=options['marker']
                added.append(dictionary)
#         fig.marks = fig.marks + [(bqplot.Lines(x=np.arange(5), y=np.arange(5), scales={'x': sc_x, 'y': sc_y},
#                    colors=[GOLDENROD]))]
                
                
                
                
        inp = output
    for marks in fig.marks:
        marks.default_size = 120
    display_widgets = widgets.interactive(wrapped, **interact_params)
    display(display_widgets)
    display(fig)

In [11]:
DARK_BLUE = '#475A77'
GOLDENROD = '#FEC62C'
BRICK_RED = '#B22222'

In [12]:
def cute_shape(size, plotting_info, time, ratio):
    # build attribute grid where if there is no shape it is a 0, and dict of attribute where there is a shape
    shape_color_mapping = {'circle': GOLDENROD, 'triangle-up': DARK_BLUE}
    attribute_grid = [[0] * size for _ in range(size)]
    for coords_and_options in plotting_info:
        coords = coords_and_options['coords']
        options = coords_and_options['options']
        for i,j in coords:
            attribute_grid[size-j-1][i] = options.copy()
    
    # check if the neighbor is satisfied
    def neighbors_are_similar(x,y):
        attribute = attribute_grid[x][y] 
        similar_count = different_count = 0
        for i in range(max(0, x-1), min(size, x+2)):
            for j in range(max(0, y-1), min(size, y+2)):
                if attribute_grid[i][j] != 0 and (i != x or j != y):
                    if attribute['marker'] == attribute_grid[i][j]['marker']:
                        similar_count += 1
                    else:
                        different_count += 1
        return different_count+similar_count == 0 or similar_count/(different_count+similar_count) > ratio
    
    # set the happiness colors for all shapes
    if time ==0:
        for i in range(size):
            for j in range(size):
                options = attribute_grid[i][j]
                if options != 0:
                    if not neighbors_are_similar(i, j):
                        attribute_grid[i][j]['color'] = BRICK_RED
                    else:
                        attribute_grid[i][j]['color'] = shape_color_mapping[attribute_grid[i][j]['marker']]
                
    # for visualizing the board changing
    
    if time !=0 and time < 1000:
        #find the unhappy shape
        unhappy_shape_coord = None
        unhappy_shape = None
        for i in range(size):
            if unhappy_shape:
                break
            for j in range(size):
                if attribute_grid[i][j] != 0 and attribute_grid[i][j]['color'] == BRICK_RED:
                    unhappy_shape = attribute_grid[i][j]
                    unhappy_shape_coord = [i,j]
        displaced = True
        if unhappy_shape:
            while displaced:
                new_coord = [np.random.randint(0, size), np.random.randint(0, size)]
                if attribute_grid[new_coord[0]][new_coord[1]] == 0:
                    attribute_grid[unhappy_shape_coord[0]][unhappy_shape_coord[1]] = 0
                    attribute_grid[new_coord[0]][new_coord[1]] = unhappy_shape
                    displaced = False
            for i in range(size):
                for j in range(size):
                    options = attribute_grid[i][j]
                    if options != 0:
                        if not neighbors_are_similar(i, j):
                            attribute_grid[i][j]['color'] = BRICK_RED
                        else:
                            attribute_grid[i][j]['color'] = shape_color_mapping[attribute_grid[i][j]['marker']]
#         else:
#             for i in range(size):
#                 for j in range(size):
#                     options = attribute_grid[i][j]
#                     if options != 0:
#                         attribute_grid[i][j]['color'] = shape_color_mapping[attribute_grid[i][j]['marker']]

    # change grid to output format
    merged_options_coordinates = []
    for i in range(size):
        for j in range(size):
            options = attribute_grid[i][j]
            if options != 0:
                added = False
                for dictionary in merged_options_coordinates:
                    if dictionary['options'] == options:
                        dictionary['coords'].append([j, size-i-1])
                        added = True
                        break
                if not added:
                    merged_options_coordinates.append({'options':options, 'coords':[[j, size-i-1]]})
    return merged_options_coordinates

In [13]:
import itertools

In [14]:
np.random.seed(43)
coordinates = list(set(itertools.combinations(np.append(np.arange(15),np.arange(15)), 2)))
np.random.shuffle(coordinates)
inp= [{'options': {'color':'blue', 'marker':'triangle-up'}, 'coords':coordinates[0:40]},
      {'options': {'color':'blue', 'marker':'circle'}, 'coords':coordinates[40:80]}]

In [15]:
#widgets.Play(value=0, max=100)

In [16]:
shape_grid(4, cute_shape, 15, inp, time=widgets.Play(value=0, max=100), ratio=widgets.FloatSlider(value=0.3, min=0, max=1))



In [ ]: