Skip to content

Commit

Permalink
Scorechart, color updates.
Browse files Browse the repository at this point in the history
* Introduce new batch script to interpolate colors and extract
  color scheme information from CSS.
* Update scheme colors for pink-to-red, orange-to-blue using oklch
  interpolation.
* Scorechart_Page can generate charts in any scheme.
* Scorechart_Page can print larger images (`scale` parameter).
  • Loading branch information
kohler committed Nov 5, 2023
1 parent f41caef commit 7a9f186
Show file tree
Hide file tree
Showing 6 changed files with 273 additions and 57 deletions.
163 changes: 163 additions & 0 deletions batch/colorschemes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php
// colorschemes.php -- HotCRP script for analyzing color schemes
// Copyright (c) 2006-2023 Eddie Kohler; see LICENSE.

if (realpath($_SERVER["PHP_SELF"]) === __FILE__) {
require_once(dirname(__DIR__) . "/src/init.php");
exit(ColorSchemes_Batch::make_args($argv)->run());
}

class ColorSchemes_Batch {
/** @var array<string,array{0|1|2,int,?string}> */
public $schemes = [];
/** @var array<string,list<string>> */
public $colors = [];
/** @var ?list<string> */
public $gradient;
/** @var bool */
public $php = false;

function __construct($arg) {
if (isset($arg["gradient"])) {
$this->gradient = $arg["gradient"];
} else if (isset($arg["php"])) {
$this->php = true;
}
foreach (Discrete_ReviewField::$scheme_info as $name => $sch) {
$this->schemes[$name] = $sch;
if ($sch[0] !== 1) {
$this->colors[$name] = array_fill(0, $sch[1], null);
}
}
$this->colors["none"][0] = "222222";
}

/** @param string $s
* @return OklchColor */
function from_hashcolor($s) {
$p = strlen($s) === 7 ? 1 : 0;
return OklchColor::from_rgb(intval(substr($s, $p, 2), 16),
intval(substr($s, $p + 2, 2), 16),
intval(substr($s, $p + 4, 2), 16));
}

/** @return int */
function run_gradient() {
$mode = $this->gradient[3] ?? "shorter";
if (count($this->gradient) < 3
|| count($this->gradient) > 4
|| !ctype_digit($this->gradient[0])
|| ($n = intval($this->gradient[0])) < 2
|| !preg_match('/\A#?([0-9a-fA-F]{6})\z/', $this->gradient[1], $m1)
|| !preg_match('/\A#?([0-9a-fA-F]{6})\z/', $this->gradient[2], $m2)
|| !in_array($mode, ["shorter", "longer", "increasing", "decreasing"])) {
throw new CommandLineException("`--gradient` expects NSTOPS COLOR1 COLOR2 [longer]");
}
$c1 = self::from_hashcolor($m1[1]);
$c2 = self::from_hashcolor($m2[1]);
if (is_nan($c1->okh) && is_nan($c2->okh)) {
$c1->okh = $c2->okh = 0;
} else if (is_nan($c1->okh)) {
$c1->okh = $c2->okh;
} else if (is_nan($c2->okh)) {
$c2->okh = $c1->okh;
}
$hi = HclColor::hue_interpolate($c1->okh, $c2->okh, $mode);
for ($i = 0; $i < $n; ++$i) {
if ($i === 0) {
$c = $c1;
} else if ($i === $n - 1) {
$c = $c2;
} else {
$c = $c1->interpolate($i / ($n - 1), $c2, $hi);
}
fwrite(STDOUT, strtolower($c->hashcolor()) . "\n");
}
return 0;
}

private function write_php($j) {
$col = [];
$cat = [];
$rev = [];
foreach ($this->schemes as $name => $sch) {
if ($sch[0] === 2) {
$cat[] = "\"{$name}\" => true";
}
if ($sch[0] !== 1) {
$col[] = "\"{$name}\" => \"{$j[$name]->colors}\"";
}
if ($sch[2]) {
$rev[] = "\"{$name}\" => \"{$sch[2]}\"";
}
}
fwrite(STDOUT, " public static \$scheme_colors = [" . join(", ", $col) . "];\n"
. " public static \$scheme_categorical = [" . join(", ", $cat) . "];\n"
. " public static \$scheme_reverse = [" . join(", ", $rev) . "];\n");
}

/** @return int */
function run() {
if (isset($this->gradient)) {
return $this->run_gradient();
}
$css = file_get_contents(SiteLoader::$root . "/stylesheets/style.css");
preg_match_all('/^\.sv-?([a-z]*)(\d+)\s*\{\s*color\s*:\s*#([0-9a-fA-F]{6})\s*;(?:\s|\/\*.*?\*\/)*\}/m', $css, $ms, PREG_SET_ORDER);
foreach ($ms as $mx) {
$name = $mx[1] === "" ? "sv" : $mx[1];
$idx = intval($mx[2]);
$color = strtolower($mx[3]);
if (!isset($this->schemes[$name])
|| $this->schemes[$name][0] === 1
|| $idx <= 0
|| $idx > $this->schemes[$name][1]
|| isset($this->colors[$name][$idx - 1])) {
fwrite(STDERR, "Unexpected color {$mx[0]}\n");
} else {
$this->colors[$name][$idx - 1] = $color;
}
}
$j = [];
foreach ($this->schemes as $name => $sch) {
if ($sch[0] === 1) {
continue;
}
if (!isset($this->colors[$name])
|| count(array_filter($this->colors[$name])) !== $sch[1]) {
fwrite(STDERR, "Some colors not set for scheme {$name}\n");
} else {
$j[$name] = $jx = (object) [];
if ($sch[0] === 2) {
$jx->categorical = true;
}
$jx->colors = join("", $this->colors[$name]);
}
}
if ($this->php) {
$this->write_php($j);
} else {
foreach ($this->schemes as $name => $sch) {
if ($sch[0] === 1 && isset($j[$sch[2]])) {
$j[$sch[2]]->reverse = $name;
}
}
fwrite(STDOUT, json_encode($j, JSON_PRETTY_PRINT) . "\n");
}
return 0;
}

/** @return ColorSchemes_Batch */
static function make_args($argv) {
$arg = (new Getopt)->long(
"help,h !",
"php",
"gradient[]+,g[]+ Compute gradient"
)->description("Analyze HotCRP CSS color schemes.
Usage: php batch/colorschemes.php")
->helpopt("help")
->maxarg(0)
->parse($argv);

return new ColorSchemes_Batch($arg);
}
}
2 changes: 1 addition & 1 deletion scripts/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -1481,7 +1481,7 @@ function score_ticks(rf) {
rewrite: function () {
this.selectAll("g.tick text").each(function () {
var d = d3.select(this), value = +d.text();
d.attr("fill", rf.rgb(value));
d.attr("fill", rf.color(value));
if (!rf.default_numeric && value)
d.text(rf.unparse_symbol(value, split));
});
Expand Down
10 changes: 5 additions & 5 deletions scripts/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -6010,8 +6010,8 @@ DiscreteValues_ReviewField.prototype.parse_value = function (txt) {
return si >= 0 ? this.value_info(si + 1) : null;
};

DiscreteValues_ReviewField.prototype.rgb = function (val) {
return this.scheme_info.rgb(val);
DiscreteValues_ReviewField.prototype.color = function (val) {
return this.scheme_info.color(val);
};

DiscreteValues_ReviewField.prototype.className = function (val) {
Expand Down Expand Up @@ -6100,8 +6100,8 @@ Checkbox_ReviewField.prototype.parse_value = function (txt) {
return null;
};

Checkbox_ReviewField.prototype.rgb = function (val) {
return this.scheme_info.rgb(val ? 2 : 1);
Checkbox_ReviewField.prototype.color = function (val) {
return this.scheme_info.color(val ? 2 : 1);
};

Checkbox_ReviewField.prototype.className = function (val) {
Expand Down Expand Up @@ -12720,7 +12720,7 @@ return function (n, scheme, flip) {
categorical: (sci[0] & 2) !== 0,
max: sci[1],
rgb_array: rgb_array,
rgb: function (val) {
color: function (val) {
var x = rgb_array(val);
return sprintf("#%02x%02x%02x", x[0], x[1], x[2]);
},
Expand Down
6 changes: 5 additions & 1 deletion scripts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -723,12 +723,16 @@ function rf_color() {
}
for (i = 1; i <= scheme.max && c; ++i) {
if (c.children.length < i)
$(c).append('<svg width="0.5em" height="0.75em" viewBox="0 0 1 1"><path d="M0 0h1v1h-1z" fill="currentColor" /></svg>');
$(c).append('<svg width="0.75em" height="0.75em" viewBox="0 0 1 1"><path d="M0 0h1v1h-1z" fill="currentColor" /></svg>');
c.children[i - 1].setAttribute("class", scheme.className(i));
}
while (c && i <= c.children.length) {
c.removeChild(c.lastChild);
}
/*c.append($e("br"), $e("span", {
"class": "d-inline-block",
"style": "width:" + (0.75 * scheme.max) + "em;height:1em;background:linear-gradient(in oklch to right, " + scheme.color(1) + " 0% " + (50 / scheme.max) + "%, " + scheme.color(scheme.max) + " " + (100 - 50 / scheme.max) + "% 100%)"
}));*/
}

handle_ui.on("change.rf-scheme", rf_color);
Expand Down
Loading

0 comments on commit 7a9f186

Please sign in to comment.