Hone.jl

Как я запрограммировал первое расширение Hone в Джулии

Создание и использование расширения моей графической библиотеки в Julia.

Гитхаб Репо

Введение

Одной из основных целей, на которую я хотел обратить внимание, когда изначально создавал свою библиотеку векторных графиков для Джулии, Hone.jl, было иметь модульную библиотеку графиков, которую мог бы расширить любой желающий. С помощью этой методологии пользователи смогут создавать расширения пакетов, которые расширят функциональные возможности и сделают библиотеку лучшей для универсального использования.

Создать пакет, способный делать такие вещи, легче сказать, чем сделать. Тем не менее, эта идея очень заманчива для меня, потому что на самом деле я не знаю других графических библиотек, которые бы делали такие вещи. Особенно это касается Юлии. К сожалению, прежде чем мы сможем приступить к созданию расширения Hone, нам придется немного дополнить код.

Добавление расширений в Hone

Hone обладает расширяемыми возможностями с относительно ранней версии. Эта система, однако, не совсем лучшая. Он использует параметр, называемый custom, где можно вводить объединенные теги, которые будут оцениваться как метавыражения. Есть несколько фундаментальных проблем с обработкой подобных расширений, которые снижают жизнеспособность расширений Hone.

  • Должен быть пользовательский условный
  • Входные теги должны быть либо объединены, либо представлять собой массив, который объединяется в функции.
  • Это замедлит работу функции для людей, которые не используют расширения, замедляя Hone и не принося никакой пользы.

Вот код, чтобы вы могли понять, о чем я говорю. Все ссылки на «обычай» выделены жирным шрифтом.

function _arrayscatter(x,y,shape=Circle(.5,.5,25),axiscolor=:lightblue,
        debug=false, grid=Grid(3), custom="", frame=Frame(1280,720,0mm,0mm,0mm,0mm))
   topx = maximum(x)
    topy = maximum(y)
    axisx = Line([(0,frame.height), (frame.width,frame.height)],axiscolor)
   axisx_tag = axisx.update([(-1,-1), (-1,1), (1,1)])
    axisy = Line([(0,0), (0,frame.height)],axiscolor)
    axisy_tag = axisy.update([(0,0), (0,1), (0,1)])
    grid_tag = grid.update()
    ######
    ######
    fullcustom = ""
    if custom != ""
        [custom = string(fullcustom, i) for i in custom]
    end
    expression = string("")
    # Coordinate parsing -------
    for (i, w) in zip(x, y)
        inputx = (i / topx) * frame.width
        inputy = (w / topy) * frame.height
        exp = shape.update(inputx,inputy)
        expression = string(expression,string(exp))
    end
    expression = string(expression, "(context(),", axisx_tag,grid_tag,custom, axisy_tag,"),")
    tt = transfertype(expression)
    frame.add([tt])
    if debug == true println(expression) end
    composition = eval(expression)
    show() = frame.show()
    tree() = introspect(composition)
    save(name) = draw(SVG(name), composition);
    get_frame() = frame
    (var)->(show;composition;tree;save;get_frame)
end

Для количественной оценки этот код состоит из 34 строк. Общее количество строк, относящихся к пользовательскому коду, составляет 6, а это означает, что кастомный код может быть отнесен к 17 процентам строк кода, хотя большую часть времени он вообще не используется. Назовите меня сумасшедшим, но

Я не могу с этим жить.

Кроме того, всякий раз, когда я создавал функции фрейма, я создавал метод add(), с помощью которого вы можете добавить объект в фрейм. Мне пришло в голову, что использование этого метода для добавления любого объекта расширения намного лучше, чем использование пользовательского тега, и это намного проще, чем заставлять пользователя получать теги и объединять их для использования старой системы. Итак, после очистки всех вхождений custom из функций _arrayscatter() и _dfscatter(), я решил улучшить фрейм, чтобы вместо этого иметь возможность принимать эти объекты.

add(object) = composition,objects,tag = _frameup(base,objects,object)

Метод add просто вызывает метод _frameup и предоставляет ему базовый тег для кадра, «базу», итеративный список объектов, «объекты» и объект. Раньше старый метод add() выглядел так:

add(objects) = composition,objects,tag = _frameup(base,objects)

Это означает несколько вещей. Во-первых, объекты не будут добавляться в список объектов, а будут заменять объекты. Хуже того, предоставленные объекты должны быть в массиве, чтобы функция работала (поскольку вы не можете итерировать один тип). Это просто означает, что при вызове функции вам нужно будет использовать синтаксис массива:

# This is how you would expect it to work:
frame.add(object)
# This is the change you needed to make:
frame.add([object])

Мне это не нравится, потому что предполагается, что массив присутствует для объекта, который наверняка всегда будет один. Конечно, эта функция является только маршрутизатором для метода _frameup(), так что давайте посмотрим на это:

function _frameup(tag,objects)
    for object in objects
       tag = string(tag,"(context(),",object.tag, ")")
    end
    tag = string(tag,")")
    println(tag)
    exp = Meta.parse(tag)
    composition = eval(exp)
    return(composition,objects,tag)
end

Объект просто объединяется со строкой, а затем добавляется к выражению перед анализом и оценкой. Вместо этого следует добавить объект в массив объектов, предоставленный типом. После этого строка может быть объединена с тегом каждого объекта. Вот как я это сделал:

function _frameup(tag,objects,object)
    push!(objects, object.tag)
    objecttags = ""
    for o in objects
       objecttags = string(objecttags,"(context(),",o, "),")
    end
    println("objects: ",objecttags)
    tag = string(tag,objecttags,")")
    exp = Meta.parse(tag)
    println(exp)
    composition = eval(exp)
    return(composition,objects,tag)
ends

Наконец, я собираюсь добавить метод к функциям _arrayscatter() и _dfscatter(), который будет вызывать фрейм для добавления функции:

add(obj) = frame.add(obj)

Теперь давайте проверим это!

Сначала мы создадим круг. Это объект, который мы собираемся добавить в кадр поверх нашего точечного графика. Я также пошел дальше и создал два массива одинаковой длины для анализа и построения графика рассеяния.

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

plt = Scatter(x,y,Circle(.5,.5,25,:orange),:purple)

Теперь мы можем использовать метод show():

plt.show()

И мы можем добавить этот образец формы, который мы сделали ранее, и показать снова.

plt.add(exampshape)
plt.show()

Ой,

мы забыли добавить наш метод к нашему типу:

Вы могли заметить, что, к счастью, у нас есть функция get_frame(), которую мы можем вызвать, чтобы вернуть наш кадр, и мы можем добавить его оттуда.

fr = plt.get_frame()
fr.show()

В этот момент я понял, что мы установили x и y нашей фигуры на 0,5 и 0,5, так что давайте изменим это… На этот раз мы поместим его в центр графика. На этот раз я решил использовать текст:

realshape = Hone.Text("Greetings",(1280 / 2), (720 / 2), :blue, 30)
fr.add(realshape)
fr.show()

Потрясающий!

Создание расширения

Как мы все знаем, визуализация данных и машинное обучение хорошо сочетаются друг с другом. Помня об этом, я решил сделать первое расширение для Hone на основе библиотеки машинного обучения Lathe. Это также будет домом для статистических графиков Hone, поскольку они будут использовать модуль Lathe. Дамы и господа, представляю вам

Токарный станокHE.jl

Первое, что я решил добавить в это расширение, это возможность рисовать линию наилучшего соответствия. Это будет относительно легко, так что не ждите, что я начну с чего-то сумасшедшего. Однако, чтобы это сработало, нам придется экспортировать все данные, используемые в методах разброса, чтобы к ним мог получить доступ любой, кто делает расширение, включающее графики. Вернувшись в Hone, я просто добавил эти данные в тип:

Теперь вернемся к нашему примеру наилучшего соответствия. Мы можем получить данные из нашего графика, используя plt.x и plt.y. Чтобы сделать линию наилучшей подгонки, мы можем использовать несколько подходов:

  • Соответствуйте модели линейной регрессии.
  • разделите данные и получите среднее значение каждой части указанных данных, затем нарисуйте линию с этими точками.
  • Постройте квартили линейно.

Я решил выбрать второй вариант, так как полагал, что он даст наилучшие результаты, но будет наименее требовательным к производительности. В Lathe.preprocess есть функция SortSplit. Чтобы просмотреть его документацию, нам нужно сначала импортировать его в нашу текущую среду.

using Lathe.preprocess

Затем мы можем найти его в панели документации.

Юнона такая удобная…

Первым шагом, конечно же, будет получение наших данных из наших параметров, вот как я настроил функцию:

function BestFit(plt, divisions = 5, color = :lightblue, weight = 2)
        frame = plt.get_frame()
        x = plt.x
        y = plt.y

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

divisionamount = 1 / divisions

Затем мы используем цикл while, который постоянно добавляет сумму деления к переменной, которая будет обрабатывать логику для каждого деления.

lower = 75
        totaldivisions = 0
        arrays = []
        while totaldivisions < 1
            top, lower = SortSplit(y,divisionamount)
            append!(arrays,top)
            totaldivisions += divisionamount
        end

Все, что мы здесь делаем, — это добавляем в массив небольшие разделенные версии наших массивов. Итак, далее нам нужно будет что-то сделать с указанным массивом.

xmeans = [a = mean(a) for a in arrays]

Мы получим среднее значение из массива и назначим его переменной с именем xmeans. Затем мы проделаем то же самое для нашего значения y.

lower = 75
        totaldivisions = 0
        arrays = []
        while totaldivisions < 1
            top, lower = SortSplit(y,divisionamount)
            append!(arrays,top)
            totaldivisions += divisionamount
        end
        ymeans = [a = mean(a) for a in arrays]

Далее, чтобы выполнить арифметические действия для масштабирования, нам понадобится вершина y, а также вершина X.

topy = maximum(y)
topx = maximum(x)

Теперь мы собираемся скопировать цикл, генерирующий пары из двух массивов, из функции Linear() в Hone. Если вы хотите узнать больше об этой функции, я написал об этом статью здесь:



pairs = []
        first = true
        for (i,w) in zip(xmeans,ymeans)
            if first == true
                x = 0
                first = false
            else
                x = (i / topx * frame.width)
            end
            y = (w / topy * frame.height)
            pair = Tuple([x,y])
            push!(pairs,pair)
        end

Единственное отличие в этом цикле состоит в том, что есть новая переменная и условное выражение, вызываемое first, которое будет определять, находимся ли мы в первом цикле или нет. Причина, по которой я это сделал, заключается в том, что линия всегда будет идти с левой стороны. Другой способ, которым мы могли бы сделать здесь крестики, — это разделить высоту кадра на количество делений, а затем прибавить исходную величину к самой себе, пока мы не достигнем количества делений. Это гарантирует, что линия всегда будет функцией, которая начинается с нуля и заканчивается в конце кадра.

Наконец, мы напечатаем фрейм как массив кортежей, создадим объект строки, поместим его в тип передачи и вместо того, чтобы возвращать тип, мы просто добавим его во фрейм.

pairs = Array{Tuple{Float64,Real},1}(pairs)
        lin = Line(pairs,color,weight)
        expression = string("(context(),",lin.update(:foo),")")
        tt = Hone.transfertype(expression)
        frame.add(tt)
end

Теперь давайте попробуем!

plt = Scatter(x,y,Circle(.5,.5,25,:orange),:purple)
include("src/LatheHE.jl")
using Main.LatheHE
LatheHE.BestFit(plt,2)
plt.show()

В будущем я могу подумать о том, чтобы заполнить X, как я говорил ранее, а не так, поскольку у нас действительно есть много совпадений, поскольку X являются средством нашего разделения.

Вывод

Я ТАК взволнован потенциалом расширений Hone! Вы можете делать что угодно: от создания пользовательской графики для построения данных до добавления новых функций на график. Кроме того, все становится полностью индивидуальным, потому что мы используем теги для рендеринга всего. Все, что может быть правильно скомпоновано, также может быть добавлено к любому кадру Hone.

Я вижу будущее с большим количеством расширений. Может быть, некоторые из них просты и просто добавляют кучу разных крутых легенд, которые вы можете использовать. Может быть, некоторые или более сложные и на самом деле анимируют ваши сюжеты или добавляют интерактивность Javascript — возможности безграничны! Если вы хотите начать создавать свои собственные расширения Hone, вы можете добавить ветку #Unstable, чтобы получить последнюю версию (0.0.4) и начать играть с ней!



«emmettgb/Hone.jl
Модульные формы, сетки и линии. Простые сборные конструкции.