

This is an evolution of Periodic System v1 and Periodic System v2, and they’re all based on another project of mine called Periodic System. This version bumps the number of elements up to 1024, and orders them in outward spiral. It also attempts to apply relevant colors to heavier element based on the chemical series they would be assumed to fall under, even though it is unlikely these elements could ever exist, at least under normal conditions.
import numpy as np from PIL import Image, ImageDraw, ImageFont import colorsys from collections import defaultdict from scipy.ndimage import binary_dilation import random # === Constants === WIDTH, HEIGHT = 4096, 4096 TOTAL_COLORS = WIDTH * HEIGHT ELEMENT_COUNT = 1024 GRID_COLS = 32 GRID_ROWS = (ELEMENT_COUNT + GRID_COLS - 1) // GRID_COLS CELL_W = WIDTH // GRID_COLS CELL_H = HEIGHT // GRID_ROWS TEXT_MARGIN = 4 # === Fonts === font_small = ImageFont.truetype("DejaVuSans-Bold.ttf", 12) font_regular = ImageFont.truetype("DejaVuSans-Bold.ttf", 16) font_bold = ImageFont.truetype("DejaVuSans-Bold.ttf", 32) # === Known data === ELEMENT_SYMBOLS = [ # Z=1 to 118 "H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og" ] ELEMENT_NAMES = [ # Z=1 to 118 "Hydrogen", "Helium", "Lithium", "Beryllium", "Boron", "Carbon", "Nitrogen", "Oxygen", "Fluorine", "Neon", "Sodium", "Magnesium", "Aluminium", "Silicon", "Phosphorus", "Sulfur", "Chlorine", "Argon", "Potassium", "Calcium", "Scandium", "Titanium", "Vanadium", "Chromium", "Manganese", "Iron", "Cobalt", "Nickel", "Copper", "Zinc", "Gallium", "Germanium", "Arsenic", "Selenium", "Bromine", "Krypton", "Rubidium", "Strontium", "Yttrium", "Zirconium", "Niobium", "Molybdenum", "Technetium", "Ruthenium", "Rhodium", "Palladium", "Silver", "Cadmium", "Indium", "Tin", "Antimony", "Tellurium", "Iodine", "Xenon", "Caesium", "Barium", "Lanthanum", "Cerium", "Praseodymium", "Neodymium", "Promethium", "Samarium", "Europium", "Gadolinium", "Terbium", "Dysprosium", "Holmium", "Erbium", "Thulium", "Ytterbium", "Lutetium", "Hafnium", "Tantalum", "Tungsten", "Rhenium", "Osmium", "Iridium", "Platinum", "Gold", "Mercury", "Thallium", "Lead", "Bismuth", "Polonium", "Astatine", "Radon", "Francium", "Radium", "Actinium", "Thorium", "Protactinium", "Uranium", "Neptunium", "Plutonium", "Americium", "Curium", "Berkelium", "Californium", "Einsteinium", "Fermium", "Mendelevium", "Nobelium", "Lawrencium", "Rutherfordium", "Dubnium", "Seaborgium", "Bohrium", "Hassium", "Meitnerium", "Darmstadtium", "Roentgenium", "Copernicium", "Nihonium", "Flerovium", "Moscovium", "Livermorium", "Tennessine", "Oganesson" ] def generate_symbol(n): if n <= 118: return ELEMENT_SYMBOLS[n - 1] roots = ['n','u','b','t','q','p','h','s','o','e'] return ''.join(roots[int(d)] for d in str(n)).capitalize()[:3] def generate_name(n): if n <= 118: return ELEMENT_NAMES[n - 1] roots = ['nil','un','bi','tri','quad','pent','hex','sept','oct','enn'] digits = [int(d) for d in str(n)] parts = [roots[d] for d in digits] if parts[-1] in ('bi', 'tri'): parts[-1] = parts[-1][:-1] for i in range(len(parts) - 1): if parts[i] == 'enn' and parts[i + 1] == 'nil': parts[i] = 'en' return ''.join(parts).capitalize() + "ium" def generate_weight(n): return f"[{n:.1f}]" def classify_element(n): if n == 1: return "nonmetal" if n in [2,10,18,36,54,86,118]: return "noble_gas" if n in [5,14,32,33,51,52,84]: return "metalloid" if n in [9,17,35,53,85,117]: return "halogen" if n in [6,7,8,15,16,34]: return "nonmetal" if n in [3,11,19,37,55,87,119]: return "alkali" if n in [4,12,20,38,56,88,120]: return "alkaline" if 57 <= n <= 71: return "lanthanide" if 89 <= n <= 103: return "actinide" if 104 <= n <= 112 or (21 <= n <= 30) or (39 <= n <= 48) or (72 <= n <= 80): return "transition" if (13 <= n <= 16) or (31 <= n <= 34) or (49 <= n <= 52) or (81 <= n <= 84): return "post_transition" if 121 <= n <= 138: return "actinide" if 139 <= n <= 152: return "transition" if 153 <= n <= 172: return "post_transition" if 173 <= n <= 200: return "noble_gas" if n % 18 == 2 else "halogen" if n > 200: return "unknown" return "unknown" # === Spiral grid generator === def generate_spiral_grid(cols, rows): grid = [[-1] * cols for _ in range(rows)] cx, cy = cols // 2, rows // 2 x, y = cx, cy dx = [1, 0, -1, 0] dy = [0, 1, 0, -1] steps = 1 dir_idx = 0 count = 1 while count <= cols * rows: for _ in range(2): for _ in range(steps): if 0 <= x < cols and 0 <= y < rows: grid[y][x] = count count += 1 if count > cols * rows: return grid x += dx[dir_idx] y += dy[dir_idx] dir_idx = (dir_idx + 1) % 4 steps += 1 return grid spiral_grid = generate_spiral_grid(GRID_COLS, GRID_ROWS) position_by_element = {} for y in range(GRID_ROWS): for x in range(GRID_COLS): n = spiral_grid[y][x] if n != -1 and n <= ELEMENT_COUNT: position_by_element[n] = (x, y) # === Reserve layout === image_array = np.zeros((HEIGHT, WIDTH, 3), dtype=np.uint8) used_pixels = np.zeros((HEIGHT, WIDTH), dtype=bool) label_coords, outline_coords, border_coords = [], [], [] cell_fill_coords = defaultdict(list) series_element_ids = defaultdict(list) for n in range(1, ELEMENT_COUNT + 1): col, row = position_by_element[n] x0, y0 = col * CELL_W, row * CELL_H lines = [ (str(n), font_regular), (generate_symbol(n), font_bold), (generate_weight(n), font_regular), (generate_name(n), font_small) ] y_cursor = y0 + TEXT_MARGIN for text, font in lines: mask = Image.new("1", (CELL_W, CELL_H), 0) draw = ImageDraw.Draw(mask) draw.text((TEXT_MARGIN, 0), text, font=font, fill=1) arr = np.array(mask) outline = binary_dilation(arr) & ~arr for dy in range(arr.shape[0]): for dx in range(arr.shape[1]): px, py = x0 + dx, y_cursor + dy if 0 <= px < WIDTH and 0 <= py < HEIGHT: if arr[dy, dx]: label_coords.append((py, px)) used_pixels[py, px] = True elif outline[dy, dx]: outline_coords.append((py, px)) used_pixels[py, px] = True y_cursor += font.size + 4 for x in range(x0, x0 + CELL_W): for y in (y0, y0 + CELL_H - 1): if not used_pixels[y, x]: border_coords.append((y, x)) used_pixels[y, x] = True for y in range(y0 + 1, y0 + CELL_H - 1): for x in (x0, x0 + CELL_W - 1): if not used_pixels[y, x]: border_coords.append((y, x)) used_pixels[y, x] = True series = classify_element(n) series_element_ids[series].append(n) for y in range(y0, y0 + CELL_H): for x in range(x0, x0 + CELL_W): if not used_pixels[y, x]: cell_fill_coords[n].append((y, x)) used_pixels[y, x] = True # === Color allocation === all_colors = np.array([[r, g, b] for r in range(256) for g in range(256) for b in range(256)], dtype=np.uint8) def brightness(rgb): return 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2] def rgb_to_hsv(rgb): return colorsys.rgb_to_hsv(rgb[0]/255, rgb[1]/255, rgb[2]/255) sorted_by_brightness = sorted(all_colors.tolist(), key=brightness) label_colors = sorted_by_brightness[:len(label_coords)] outline_colors = sorted_by_brightness[-len(outline_coords):] border_colors = sorted_by_brightness[len(label_colors):-len(outline_coords)][:len(border_coords)] used_colors = set(tuple(c) for c in label_colors + outline_colors + border_colors) for (py, px), c in zip(label_coords, label_colors): image_array[py, px] = c for (py, px), c in zip(outline_coords, outline_colors): image_array[py, px] = c for (py, px), c in zip(border_coords, border_colors): image_array[py, px] = c remaining_colors = [tuple(c) for c in all_colors.tolist() if tuple(c) not in used_colors] remaining_sorted = sorted(remaining_colors, key=lambda c: (-rgb_to_hsv(c)[1], rgb_to_hsv(c)[0], brightness(c))) series_hue_ranges = { 'transition': (0.00, 0.10), 'noble_gas': (0.50, 0.60), 'halogen': (0.30, 0.40), 'alkali': (0.90, 1.00), 'alkaline': (0.10, 0.20), 'lanthanide': (0.55, 0.65), 'actinide': (0.65, 0.75), 'post_transition': (0.20, 0.30), 'metalloid': (0.75, 0.85), 'nonmetal': (0.05, 0.10) } series_rgb_pool = defaultdict(list) unassigned_pool = [] for c in remaining_sorted: h, s, _ = rgb_to_hsv(c) for sname, (hmin, hmax) in series_hue_ranges.items(): if hmin <= h <= hmax: series_rgb_pool[sname].append(c) break else: unassigned_pool.append(c) used_known_colors = set() for series, ids in series_element_ids.items(): if series in series_hue_ranges: coords = sum((cell_fill_coords[n] for n in ids), []) needed = len(coords) pool = series_rgb_pool[series] if len(pool) < needed: pool += unassigned_pool[:needed - len(pool)] unassigned_pool = unassigned_pool[needed - len(pool):] random.shuffle(pool) for (y, x), c in zip(coords, pool[:needed]): image_array[y, x] = c used_known_colors.add(c) remaining_pool = [c for c in remaining_sorted if c not in used_known_colors and c not in used_colors] cursor = 0 for n in series_element_ids.get("unknown", []): coords = cell_fill_coords[n] chunk = remaining_pool[cursor:cursor + len(coords)] cursor += len(coords) random.shuffle(chunk) for (y, x), c in zip(coords, chunk): image_array[y, x] = c used_pixel_count = np.count_nonzero(np.any(image_array != 0, axis=2)) print(f"\n? Total used pixels: {used_pixel_count} / {TOTAL_COLORS}") Image.fromarray(image_array, 'RGB').save("periodic_table_allrgb_spiral.png") print("✅ Saved as periodic_table_allrgb_spiral.png")
Date | |
---|---|
Colors | 16,777,216 |
Pixels | 16,777,216 |
Dimensions | 4,096 × 4,096 |
Bytes | 48,468,984 |