Перейти к основному содержимому

Генератор pattern

Когда пригодится: у сигнала есть форма, которую не описать формулой — спрос по часам, демографическая волна, скриншот биржевого графика, любая «извилистая» линия. Вы рисуете картинку, а её форма растягивается на все строки: горизонтальная ось — номер строки (от первой до последней), вертикальная — значение. Рисунок может быть крошечным, строк — миллионы.

Никакой файл заранее не готовится. Картинка читается в момент запуска, и каждая строка сама вычисляет свою точку на ней, поэтому миллион строк стоит не больше памяти, чем десяток.

Примеры вывода ниже — иллюстративные, по версиям ядра числа могут чуть отличаться; гарантируется форма.

Зачем рисовать форму, а не брать распределение из списка

У готовых распределений (normal, poisson, …) форма фиксированная — колокол, затухающий хвост. Настоящему сигналу часто нужна форма, которой в этом списке нет: два горба, где правый выше, плато со всплеском в конце, спрос, который растёт к полудню и падает ночью. «Два горба разной высоты» из normal не получить. pattern снимает это ограничение: силуэт вы рисуете сами, и он ложится на строки как есть.

Как включить

Каждый <gen> с type="pattern" берёт ровно один источник формы. Обычный источник — файл: рисуете в любом редакторе или делаете скриншот графика и отдаёте его генератору:

<!-- ваш рисунок: SVG или PNG -->
<gen type="pattern" src="chart.svg" y_range="0..40"/>

Если форма достаточно простая, чтобы набрать её руками, тот же генератор принимает точки прямо в конфиге — одна линия через points или полоса через upper / lower:

<gen type="pattern" points="0,5 20,40 40,8 60,45 80,15 100,5" y_range="0..40"/>
<gen type="pattern" upper="0,20 50,40 100,20" lower="0,5 50,10 100,5" y_range="0..40"/>
АтрибутЧто задаёт
srcФайл SVG или PNG — обычный способ задать форму
pointsПары x,y прямо в конфиге вместо файла: x — по ширине, y — высота
upper / lowerДве границы прямо в конфиге — коридор
modesignal (по умолчанию) — траектория; densityраспределение
y_rangeОбязателен. min..max — чем становятся пол и потолок холста рисунка
fitниз..верх — куда ложится рисунок из src (ниже)
interplinear (по умолчанию) / smooth / step — поведение линии между точками
spreadРазброс: каждая строка ±N в единицах y_range (по умолчанию 0 — точная линия)
ink_threshold0..1 — насколько тёмным должен быть пиксель PNG, чтобы считаться чернилами (по умолчанию 0.5)
decimalsЗнаков после запятой (по умолчанию 0)

src — картинка и есть конфиг

Читаются два формата, и оба внутри превращаются в одно и то же — векторный контур:

  • SVG — читается по настоящей геометрии: <path>, <polyline>, <polygon>, <line>, <rect> (скруглённые углы включительно), <circle>, <ellipse>. Кривые Безье и дуги проходятся так, как нарисованы, а transform на фигуре или на любой объемлющей <g> применяется. Рисуйте в Illustrator, Figma, Inkscape или руками — экспортируйте и работайте. Фигуры различает одно правило: когда читается одиночная линия, примитивы (rect, circle, ellipse) считаются мебелью — рамкой и фоном экспортированного графика — и всё действительно нарисованное важнее их; в коридоре считается каждая фигура, потому что рисунок и есть область.
  • PNG с прозрачным фоном — нарисованные пиксели и есть форма, прозрачность — пустота. Именно так выглядит обрезанный по графику скриншот или мазок кистью из любого растрового редактора. Если у PNG сплошной фон, включается запасное правило: чернила — тёмные пиксели, светлые — фон, а порог двигает ink_threshold.

Ось y в SVG растёт вниз — так её хранит любая программа для рисования. TDC переворачивает её, поэтому «выше на экране» всегда означает «больше значение».

Целый пример, от начала до конца

Вот настоящий файл — 240×140 пикселей, штрих на прозрачном фоне и больше ничего. Ни осей, ни подписей, ни рамки: всё непрозрачное в файле читается как часть рисунка.

line-input.png — одна волнистая линия на прозрачном фоне
line-input.png — весь файл целиком. Белая подложка здесь только для того, чтобы линию было видно: у самого файла фона нет.
<gen type="pattern" src="line-input.png" y_range="0..100"/>

Прогоняем на 40 строк и накладываем результат на рисунок. Зелёные точки — это сгенерированные значения, оранжевая линия — картинка:

Сгенерированные значения ложатся точно на нарисованную кривую
Каждая строка попадает ровно на штрих — у одиночной линии нет никакой случайности.
  • линия, нарисованная в файле
  • сгенерированные значения (40 строк)

В картинке 240 пикселей ширины, а в прогоне 40 строк, и эти числа никак не связаны: просите 40 строк или 4 000 000 — тот же рисунок растянется на них.

То же самое из векторного файла

SVG не аппроксимируется. Вот файл с одной кубической кривой Безье внутри <g>, который переворачивает ось y, — две вещи, которые редакторы выдают постоянно:

curve-input.svg — одна кривая Безье внутри переворачивающего transform
curve-input.svg — один path, один transform.
Сгенерированные значения идут точно по кривой Безье с учётом transform
Кривая проходится как нарисована: Безье обходится по-настоящему, transform применяется, ось y возвращается на место.
  • кривая Безье из файла, с применённым transform
  • сгенерированные значения (40 строк)

Рисунок читается по столбцам

TDC никогда не гадает, что у вас на картинке — «одна линия» или «полоса». Он измеряет, и измерение само отвечает на этот вопрос.

Для каждого столбца рисунка берутся два замера: сверху вниз до первой встреченной отметки и снизу вверх до первой встреченной отметки.

Два замера на столбец: сошлись в одном пикселе — точное значение, разошлись — полоса
Те же два замера на одиночной линии и на полосе. Больше ничего не анализируется.
  • Aзамеры сошлись в одном пикселе — значение точное
  • Bзамеры разошлись — значение случайное между ними
  • сами замеры: сверху вниз и снизу вверх
  • Оба замера пришли в одну и ту же точку → этот столбец даёт точное значение на графике. Все строки, попавшие сюда, получат его без всякой случайности.
  • Замеры разошлись → этот столбец — полоса. Строка получит случайное значение между двумя краями, зафиксированное seed.

Поэтому картинка из двух штрихов — это коридор, и каждая строка берёт случайное значение между ними. Вот входной файл и 300 строк, сгенерированных по нему:

tunnel-input.png — два штриха, образующие полосу
tunnel-input.png — два штриха, и больше ничего.
300 сгенерированных значений внутри нарисованной полосы
Все значения лежат внутри полосы. Где полоса широкая — разброс широкий, где сужается — сужается и он.
  • полоса, нарисованная в файле
  • сгенерированные значения (300 строк)
  • Aодин столбец: значение берётся между его краями

Два следствия, которые стоит знать:

  • Одна картинка может быть и тем и другим. Линия, которая сначала идёт одна, а потом раздваивается, до развилки даёт точные значения, а после — случайные. Ни атрибута, ни выбора режима не нужно.
  • Всё замкнутое — это коридор. Нарисуйте машину, лист, кляксу: у неё есть верхний край и нижний, значит она читается как полоса между ними. Это не фигура речи — вот машина:
car-input.png — залитый силуэт машины
car-input.png — силуэт, залитый сплошняком.
Сгенерированные значения заполняют силуэт машины
Значения заполняют кузов между верхним и нижним краем — а там, где рисунок вырождается в одну линию (земля до и после машины), они схлопываются в одно точное значение.
  • нарисованная машина
  • сгенерированные значения (300 строк)
  • Aколесо опускает нижний край кузова
  • Bземля до и после машины — одна линия, значения точные

Вот линия, которая идёт ровно, а потом раздваивается — одна ветка вверх, другая вниз:

split-input.png — линия, которая на середине раздваивается
split-input.png — один штрих, который становится двумя.
До развилки значения точные, после неё — случайные внутри полосы
До развилки каждая строка получает одно и то же точное значение, после — случайное внутри расходящейся полосы.
  • линии из файла
  • сгенерированные значения (60 строк)
  • Aздесь линия раздваивается
  • Bдо развилки — одно и то же точное значение
  • Cпосле — случайное внутри расходящейся полосы

Тот же прогон на двух разных сидах показывает это в лоб — первая половина одинаковая, вторая нет:

./run split.tdc (11 строк, два сида)
seed A:  50  50  50  50  50  50  44  47  66  18  45
seed B:  50  50  50  50  50  51  57  61  60  81  35

Коридор прямо в конфиге: два атрибута, а не одна строка

Коридору, нарисованному в файле, атрибуты вообще не нужны — два штриха на картинке уже читаются как две границы. Набранному прямо в конфиге нужны два атрибута, upper и lower, по одной кривой в каждом:

<gen type="pattern" upper="0,80 100,80" lower="0,20 100,20" y_range="0..100"/>

Соблазнительное сокращение — один points с разделителем между двумя линиями — не существует, и ; не проглатывается, а получает отказ:

tdcv2 check corridor.tdc
error[TDC285]: pattern: ";" does not separate two lines in points= — every number is read as one curve. For a band, draw the two edges separately: upper="0,80 100,80" lower="0,20 100,20".

Та же проверка ловит рисунок нулевой ширины — все точки на одном x:

tdcv2 check flat.tdc
error[TDC285]: pattern: every point sits at x=50, so the drawing has no width and a card has nothing to read across. Give the points at least two different x coordinates.

Оба отказа выдаёт и check, и прогон: points, upper и lower читаются до первой карточки тем же кодом, что и в прогоне.

y_range — масштаб, и он обязателен

У рисунка нет собственного масштаба. Одна и та же кривая выходит из одного редактора в координатах 0..100, из другого — 0..480, из третьего — 0..10002345345, и ни одно из этих чисел ничего не значит, пока вы не скажете, что такое ось. y_range="min..max" — это и есть такое утверждение, поэтому атрибут обязательный, а не необязательный: без него любой ответ был бы догадкой о чужих настройках экспорта.

Масштабируется холст, а не чернила:

откуда рисунокпол холстапотолок холста
PNGниз изображенияверх изображения
векторный файл (SVG)самая нижняя точка самого рисункасамая верхняя точка самого рисунка
points / upper / lower0, растёт вниз, если вы рисовали ниже100, растёт вверх, если вы рисовали выше

Для рисунка из точек холст — это процентная доска: по умолчанию 0..100, и она только растёт, чтобы вместить нарисованное снаружи, но никогда не сжимается. Именно это делает горизонтальную линию на 50 серединой любого запрошенного диапазона. Мерь мы по чернилам — та же линия была бы самым высоким, что есть, а значит потолком.

Три строки разные, потому что три источника несут разное. PNG ровно такой высоты, какой себя объявляет, — его рамка настоящий холст, и пустое место над штрихом честно опускает его значения. У векторного файла рамки, которой стоило бы верить, нет: большинство редакторов при экспорте обрезают viewBox по рисунку, а обрезанный не отличить от намеренно тесного. Поэтому меряется сам рисунок, а поставить его на место — дело fit.

<gen type="pattern" points="0,0 50,100 100,0" y_range="0..40" decimals="1"/>
<gen type="pattern" points="0,0 50,100 100,0" y_range="-2..2" decimals="1"/>
./run yrange.tdc (один треугольник, 11 строк)
0..40    0.0   8.0   16.0   24.0   32.0   40.0   32.0   24.0   16.0   8.0   0.0
-2..2   -2.0  -1.2   -0.4    0.4    1.2    2.0    1.2    0.4   -0.4  -1.2  -2.0

За диапазон не выходит ничего, в каких бы координатах вы ни рисовали. Сочетайте с decimals, когда нужны непрерывные значения, а не целые.

Один и тот же треугольник при трёх разных y_range
Один рисунок, три масштаба. Форма одинаковая, меняются только числа на оси.
  • один и тот же нарисованный треугольник
  • сгенерированные значения (25 строк)

y_range всегда означает диапазон значений, которые вы получите. В режиме mode="density" ось значений — это ширина рисунка, а не высота, поэтому там диапазон растягивается по картинке слева направо: смысл атрибута тот же, меняется только ось.

fit — куда ложится рисунок из файла

Рисунок несёт форму. Положение он несёт только вместе с доской — а файл, выгруженный из чужого редактора, доски не несёт. Ни единиц, ни начала координат, ни даже того, где верх. Измерить у него можно ровно две вещи: его собственную нижнюю точку и его собственную верхнюю.

fit="низ..верх" говорит, чем эти две станут.

<gen type="pattern" src="foreign.svg" y_range="200..300" fit="255..297"/>

Нижняя точка рисунка выйдет как 255, верхняя — как 297, и ничто между ними за эту полосу не выйдет, в каких бы координатах файл ни рисовали — 0..1000 или -5..3e6. Форма сохраняется, а положение — то, которое вы объявили.

Не пишите fit — и рисунок займёт весь y_range: низ на пол, верх на потолок. Это не догадка, а объявленное соглашение: оно ничего не выдумывает.

холст (что отображается)без fitс fit
векторный файлсам рисунокзанимает весь y_rangeего края ложатся на полосу
PNGрамка изображениярамка занимает весь y_rangeрамка ложится на полосу

При mode="density" он идёт за той же осью, что и y_range. Там ось значений — это ширина рисунка, а не высота, поэтому fit кладёт его левый и правый край, а не низ и верх. fit — это просто более узкий y_range для рисунка из src: ось он меняет ровно там, где её меняет y_range, и никогда отдельно.

Рядом с points / upper / lower атрибут отклоняется (TDC300). Набранная точка — это уже процент доски 0..100: 80 означает 80% от y_range, размещать больше нечего, и написать оба сразу значило бы сказать в конфиге две вещи одновременно.

Коридору из файла он нужен

Два штриха в одном файле — это коридор, а полоса, измеренная относительно самой себя, всегда полная. Поэтому без fit полосатый SVG заполняет весь y_range — по правилу выше это верно и почти никогда не то, чего хотели. fit несёт в конфиге ровно то, что набранная запись несёт в своих числах:

<gen type="pattern" upper="0,80 100,80" lower="0,60 100,60" y_range="0..100" decimals="1"/>
<gen type="pattern" src="band.svg" y_range="0..100" fit="60..80" decimals="1"/>
./run fit.tdc (один коридор, набранный и нарисованный, seed="fit", 8 строк)
набран    62.3  62.5  73.4  76.0  71.7  75.0  61.3  64.3
нарисован 62.3  62.5  73.4  76.0  71.7  75.0  61.3  64.3

Цифра в цифру — и так во всех пяти реализациях: эта пара лежит в общих фикстурах.

Выше на картинке — больше по значению

В SVG ось y растёт вниз: верх файла — это y=0. Движок её переворачивает, поэтому то, что выше в вашем редакторе, выходит больше в данных. Записывать это нигде не нужно — но полезно знать, когда рисунок вышел вверх ногами и вы полезли смотреть файл.

decimals — знаки после запятой

decimals задаёт, сколько дробных знаков остаётся у значения (по умолчанию 0, то есть целые). Пригодится, когда кривая кормит цену, температуру или любую измеряемую величину, где округление до целых сплющит форму.

<gen type="pattern" points="0,0 50,100 100,0" y_range="-1..1" decimals="2"/>
./run decimals.tdc (11 строк)
-1.00   -0.60   -0.20   0.20   0.60   1.00   0.60   0.20   -0.20   -0.60   -1.00

Средняя строка стоит на вершине и даёт 1.00 — потому что там её линия и пересекает рисунок. Ниже, в Растягивании, это правило разобрано целиком.

Растягивание — точек на рисунке почти никогда не столько, сколько строк

Строка i из count читает рисунок в точке t = i / (count − 1), и её значение — это пересечение её собственной вертикали с нарисованной линией. Десять строк — это десять пересечений, их можно проверить на картинке линейкой. Никакого второго правила под этим нет.

Деталей на рисунке больше, чем строк → вы получаете пересечения, а не сводку. Пила из пяти зубцов, ужатая в пять строк, выдаёт те пять мест, куда эти строки попали, и каждое из них — настоящая точка пилы:

<gen type="pattern" points="0,0 10,100 20,0 30,100 40,0 50,100 60,0 70,100 80,0 90,100 100,0"
y_range="0..100"/>
./run saw.tdc (5 зубцов в 5 строк)
0   50   100   50   0

Расширьте тот же прогон до 40 строк — и зубцы пройдут целиком, потому что теперь строки идут достаточно часто, чтобы попадать на них: 0 26 51 77 97 72 46 21 5 31 56 82 …

Не попасть в пик, который лежит между двумя строками, — это не потеря замера, а то, что и означает «пять строк». Рисуйте подробнее или просите больше строк: на миллионе строк тот же рисунок распишется целиком, со всеми пиками. Смысл правила в том, что вы можете посмотреть на свой рисунок заранее и сказать, что получится.

Одна и та же пила на 300 строках и на 6 строках
Одна и та же пила. Когда строк с запасом — проходит каждый зубец; когда их семь — каждая читает место, где её собственная линия пересекает рисунок, а зубцы между ними просто не попали в те семь замеров, которые запрошены.
  • нарисованная пила
  • сгенерированные значения
  • A300 строк — зубцы проходят целиком
  • B7 строк — семь пересечений, остальное между ними

Строк больше, чем точек → интерполяция, и вот тут в дело вступает interp.

interp — как ведёт себя линия между двумя точками

Между двумя далеко отстоящими точками может уместиться несколько тысяч строк. Сам по себе прямой отрезок растёт ровно на одну и ту же величину каждую строку — математически верно и заметно искусственно. interp выбирает поведение:

ЗначениеЧто делает
linear (по умолчанию)Прямые отрезки — постоянная скорость между двумя точками
smoothКривая через точки с плавным входом и выходом, без вылетов
stepДержит значение точки до следующей — лесенка

smooth использует монотонную кубическую кривую: она скругляет углы и меняет скорость, но никогда не уводит линию выше или ниже реально нарисованных значений — из изгиба не появится фантомный пик. Тот же рисунок из трёх точек на 11 строк:

./run kink.tdc (11 строк, три режима)
linear:  0   8  16  24  32  40  52  64  76  88  100
smooth:  0   8  15  23  31  40  50  62  75  88  100
step:    0   0   0   0   0  40  40  40  40  40  100

Обратите внимание, чем заканчивается step. Лесенка держит значение точки в полосе справа от неё, а у последней точки такой полосы нет — рисунок там кончается. Поэтому последняя точка читается там, куда попадает любой прогон: на самом правом краю, где его пересекает линия последней строки. Все три способа сходятся там на последнем, что вы нарисовали.

На рисунке с настоящими изломами разница видна сразу — пунктир это то, что нарисовано, точки это то, что вышло:

linear идёт по ломаной, smooth скругляет углы, step держит значение
linear едет ровно по ломаной; smooth скругляет каждый угол, ни разу не выходя за нарисованное; step держит значение точки до следующей.
  • что нарисовано: ломаная через пять точек
  • сгенерированные значения (41 строка)

Посмотрите на шаги: у linear повторяется 8, 8, 8, 8 — идеально предсказуемо. У smooth идёт 8, 7, 8, 8, 9, 10, 12, 13 — линия мягко отходит от точки, разгоняется и мягко приходит.

spread — превратить одну линию в туннель

spread="N" разбрасывает каждую строку на ±N в единицах вашего y_range. По умолчанию 0: линия точная и на 100% предсказуемая. Задайте значение — и нарисованная линия становится центром полосы, так что рисовать оба края коридора ради небольшого дрожания уже не нужно.

Разброс следует объявленному масштабу: при y_range="0..100" spread="1" — это один пункт шума, а при y_range="0..1" тот же относительный эффект даст spread="0.001".

<gen type="pattern" src="ramp.svg" y_range="0..100" spread="5"/>
./run ramp.tdc (11 строк)
spread 0, seed A:   0  10  20  30  40  50  60  70  80  90  100
spread 5, seed A:   5  13  16  29  37  52  57  69  83  86   99
spread 5, seed B:   5  11  21  26  45  53  64  73  82  94   99

Тот же одиночный штрих из line-input.png, но уже со spread="6":

Нарисованная линия с полосой ±6 вокруг и значениями внутри неё
Одна нарисованная линия — и туннель, который вы не рисовали. Форма не тронута, каждая строка просто садится куда-то в пределах ±6 от неё.
  • нарисованная линия — центр
  • коридор ±6, который вы не рисовали
  • сгенерированные значения (60 строк)

Тренд не тронут, просто каждая строка садится куда-то в пределах ±5 от него. Как и у коридора, разброс детерминирован: тот же сид воспроизводит его точь-в-точь. spread работает и поверх рисунка, который уже является полосой, — расширяет её на N в обе стороны.

ink_threshold — порог «тёмный/светлый» для PNG

Когда у PNG сплошной фон, чернила определяются темнотой. ink_threshold — это порог по шкале 0..1 (по умолчанию 0.5): пиксель считается чернилами, если он не светлее порога. Поэтому поднимайте к 1, чтобы забрать бледные сглаженные края и светло-серое, и опускайте к 0, чтобы оставить только почти чёрные штрихи. На PNG, нарисованном на прозрачности, порог не влияет ни на что: там альфа-канал уже говорит, что нарисовано.

<gen type="pattern" src="faint-curve.png" ink_threshold="0.8" y_range="0..100"/>

В этом файле два штриха на белом холсте — чёрный и светло-серый, — и порог решает, существует ли серый:

threshold-input.png — чёрный и серый штрихи на белом
threshold-input.png — два штриха на непрозрачном холсте.
При 0.5 читается только чёрный штрих, при 0.8 — оба, и получается коридор
На пороге по умолчанию серый штрих — это фон, и читается одна точная линия. Поднимите порог — серый тоже становится чернилами, краёв стало два, и значения пошли случайные между ними.
  • чёрный штрих
  • светло-серый штрих
  • сгенерированные значения (60 строк)

Сигнал — это траектория, а не гистограмма

Значение строки — это высота кривой в её позиции по порядку. То есть числа выходят вдоль кривой, как след пера: это траектория, а не «куча значений». Если взять те же числа и, забыв про порядок, построить гистограмму (как часто встречается каждое значение), форма кривой в ней не проявится. Вот настоящая гистограмма рисунка из двух горбов на 300 строках:

./run humps.tdc --count 300 | histogram
0  | ################################## 29
3  | ######################################## 34
7  | ######################################## 34
10 | #################################### 31
13 | ######################### 21
17 | ########################## 22
20 | ################################# 28
23 | ########################## 22
27 | ######################### 21
30 | ################################## 29
33 | ################### 16
37 | ############### 13

Никаких двух горбов — гистограмма почти ровная. Это не баг: сигнал проходит через каждую высоту примерно одинаково часто, поэтому куча его значений не похожа на рисунок. Если нужно, чтобы рисунок задавал частоту, — это второе прочтение, mode="density", о нём ниже.

mode — два вопроса, которые можно задать рисунку

Одна и та же картинка отвечает на два разных вопроса, mode выбирает какой:

РежимВопросЧто выходит
signal (по умолчанию)«какое значение получит эта строка0, 20, 40, 70, 90, 100, 90, 70, 40, 20, 0 — идём по линии, по порядку
density«как часто встречается это значение?»куча чисел, сгущённая там, где нарисован горб, в случайном порядке

В density оси меняются смыслом: по горизонтали — значение, а высота кривой — как часто оно встречается. Нарисовал горб посередине — большинство чисел упадёт в середину; нарисовал два горба разной высоты — тот, что выше, заберёт пропорционально больше. По сути «нарисуй свою вероятность» вместо выбора normal / poisson из списка.

<!-- треугольник, стоящий между x=25 и x=75 -->
<gen type="pattern" points="0,0 25,0 50,100 75,0 100,0" y_range="0..100" mode="density"/>
./run density.tdc --count 6000 | histogram
  0- 9 |
10-19 |
20-29 | ## 85
30-39 | ########################### 901
40-49 | ########################################################## 1911
50-59 | ############################################################ 1955
60-69 | ############################## 978
70-79 | ##### 170
80-89 |
90-99 |

Сами значения выходят вперемешку — 60 57 36 49 48 40 67 53 …, — потому что у распределения нет порядка. И за пределами 25..75 не появляется ничего: там рисунок плоский, а плоско — значит «никогда».

То же самое, но картинкой вместо набранных руками точек. Вот горб, нарисованный заливкой:

hump-input.png — залитый горб колоколом
hump-input.png — горб, намазанный кистью и сохранённый на прозрачном фоне.
<gen type="pattern" src="hump-input.png" y_range="0..100" mode="density"/>

Генерируем по нему 6000 строк и считаем, как часто выпало каждое значение:

Сверху нарисованный горб, снизу гистограмма сгенерированных значений на той же оси
Сверху — что нарисовано. Снизу — как часто на самом деле выпадало каждое значение. Горизонтальная ось одна и та же, и это значение, а не номер строки.
  • Aчто нарисовано в файле
  • Bгистограмма 6000 сгенерированных значений
  • Cвысота рисунка здесь = как часто выпадает это значение

В этом вся идея: силуэт, который вы намазали кистью, становится гистограммой данных. Никакой формулы нигде не выбиралось.

Причём высота работает пропорционально, а не просто «тут есть, тут нет»: нарисуйте один горб вдвое выше другого — он заберёт примерно вдвое больше значений:

Два горба разной высоты и соответствующая гистограмма
Два горба, правый вдвое выше. Гистограмма снизу повторяет оба — в пропорции.
  • Aчто нарисовано: два горба разной высоты
  • Bгистограмма 6000 сгенерированных значений
  • Cлевый горб
  • Dправый, вдвое выше — и значений вдвое больше

Что стоит знать про density:

  • Ноль — это пол картинки. У PNG это нижний край изображения, у SVG и точек в конфиге — самая низкая точка рисунка. Где кривая ниже всего, то значение не выпадает никогда, — поэтому доводите кривую до основания по краям нужного диапазона.
  • От полосы берётся верхний край. Если нарисован коридор, форму распределения задаёт его контур.
  • interp работает и здесь — и он реально двигает вес. smooth даёт скруглённое распределение вместо угловатого, но если рисунок — это СТОЛБЧАТАЯ диаграмма, по одной объявленной доле на столбик, берите interp="step". Только он держит значение по всей ширине столбика; linear и smooth проводят линию ЧЕРЕЗ точку, и пик растекается на соседей. Померено на десяти столбиках, 200 000 строк, пик объявлен 40%: step оставил два центральных столбика на 28.4% и 27.9%, smooth собрал их в 41.5% и 10.0%. Те же точки — другие доли, выбор не косметический.
  • spread здесь запрещён. Рисунок и так задаёт разброс, так что сочетание этих двух вещей — ошибка, а не возможность.
  • Плоский рисунок означает «мне всё равно» и даёт равномерный разброс по диапазону, а не ошибку.
  • Детерминированно и потоково, как и всё остальное: тот же сид воспроизводит ту же кучу на любом количестве строк.

Детали

  • Детерминированность: там, где рисунок — одиночная линия, значение строки просто равно высоте в этой точке (никакой случайности вообще); там, где полоса, — или там, где задан spread, — разброс зафиксирован seed. Тот же сид и конфиг → тот же результат.
  • Любой объём, любой движок: каждая строка считается из своего номера, память не растёт. Миллион строк из маленького рисунка спокойно едет на потоковых движках.
  • Файл читается один раз, в начале прогона, и превращается в геометрию — стоимость не зависит от количества строк.

Проверить руками

Несколько однострочников для интуиции:

  • points="0,0 50,100 100,0" y_range="0..100" — треугольник: значения растут к середине и падают.
  • points="0,10 90,10 100,100" y_range="0..100" — ровная линия со всплеском только в последних 10% строк.
  • upper="0,0 50,40 100,0" y_range="0..100" — полоса: случайное от 0 до центрального пика.
  • src="chart.png" y_range="0..100" — нарисованная форма становится данными вдоль контура.
  • добавьте к любому из них spread="2" — та же форма, но с дрожанием.
  • а с mode="density" та же форма начинает решать, как часто выпадает каждое значение.

Случай со всплеском лучше всего показывает идею «позиция = номер строки»:

./run spike.tdc (20 строк)
строки 1–18: 10   (ровный участок)
строка 19: 53      (подъём)
строка 20: 100     (всплеск)
Позже

Фронтенд-рисовалка, которая экспортирует ровно такие SVG/PNG, — чтобы набрасывать кривую мышью, а не открывать редактор.

См. также